From 5a97ae51ec591243c3239502dc50bbc9e6971242 Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Tue, 16 Jun 2026 11:04:54 -0400 Subject: [PATCH] wlx/dbview: Resolve PR conflicts by clean rebuild off upstream/master --- sdk/wlxplugin.h | 1 + wlx/dbview/.gitignore | 1 + wlx/dbview/CMakeLists.txt | 174 +++ wlx/dbview/README.md | 120 ++ wlx/dbview/dbview.png | Bin 0 -> 486442 bytes wlx/dbview/include/BdbEngine.h | 29 + wlx/dbview/include/DbEngine.h | 102 ++ wlx/dbview/include/DbViewWidget.h | 89 ++ wlx/dbview/include/DuckDbEngine.h | 53 + wlx/dbview/include/DuckDbModel.h | 55 + wlx/dbview/include/FirebirdEngine.h | 44 + wlx/dbview/include/KeyValueModel.h | 78 ++ wlx/dbview/include/LevelDbEngine.h | 36 + wlx/dbview/include/LmdbEngine.h | 32 + wlx/dbview/include/MdbEngine.h | 67 + wlx/dbview/include/RocksDbEngine.h | 36 + wlx/dbview/include/SqliteEngine.h | 50 + wlx/dbview/include/mdbtools/mdbfakeglib.h | 193 +++ wlx/dbview/include/mdbtools/mdbprivate.h | 51 + wlx/dbview/include/mdbtools/mdbsql.h | 112 ++ wlx/dbview/include/mdbtools/mdbtools.h | 682 +++++++++ wlx/dbview/include/mdbtools/mdbver.h | 25 + wlx/dbview/src/BdbEngine.cpp | 162 +++ wlx/dbview/src/DbEngine.cpp | 112 ++ wlx/dbview/src/DbViewWidget.cpp | 849 ++++++++++++ wlx/dbview/src/DuckDbEngine.cpp | 310 +++++ wlx/dbview/src/DuckDbModel.cpp | 300 ++++ wlx/dbview/src/FirebirdEngine.cpp | 284 ++++ wlx/dbview/src/KeyValueModel.cpp | 177 +++ wlx/dbview/src/LevelDbEngine.cpp | 142 ++ wlx/dbview/src/LmdbEngine.cpp | 227 +++ wlx/dbview/src/MdbEngine.cpp | 303 ++++ wlx/dbview/src/RocksDbEngine.cpp | 142 ++ wlx/dbview/src/SqliteEngine.cpp | 187 +++ wlx/dbview/src/libmdb/backend.c | 1219 +++++++++++++++++ wlx/dbview/src/libmdb/catalog.c | 203 +++ wlx/dbview/src/libmdb/data.c | 1146 ++++++++++++++++ wlx/dbview/src/libmdb/dump.c | 57 + wlx/dbview/src/libmdb/fakeglib.c | 616 +++++++++ wlx/dbview/src/libmdb/file.c | 501 +++++++ wlx/dbview/src/libmdb/iconv.c | 375 +++++ wlx/dbview/src/libmdb/index.c | 1150 ++++++++++++++++ wlx/dbview/src/libmdb/like.c | 91 ++ wlx/dbview/src/libmdb/map.c | 137 ++ wlx/dbview/src/libmdb/money.c | 156 +++ wlx/dbview/src/libmdb/options.c | 92 ++ wlx/dbview/src/libmdb/props.c | 230 ++++ wlx/dbview/src/libmdb/rc4.c | 85 ++ wlx/dbview/src/libmdb/sargs.c | 375 +++++ wlx/dbview/src/libmdb/stats.c | 67 + wlx/dbview/src/libmdb/table.c | 448 ++++++ wlx/dbview/src/libmdb/version.c | 24 + wlx/dbview/src/libmdb/worktable.c | 96 ++ wlx/dbview/src/libmdb/write.c | 934 +++++++++++++ wlx/dbview/src/wlx_entry.cpp | 162 +++ wlx/wlxbase_wlqt/CMakeLists.txt | 82 ++ wlx/wlxbase_wlqt/README.md | 810 +++++++++++ .../include/wlxbase_wlqt/CrashLogger.h | 74 + .../include/wlxbase_wlqt/EditableGridWidget.h | 153 +++ .../include/wlxbase_wlqt/EncodingUtils.h | 40 + .../include/wlxbase_wlqt/FilterRowWidget.h | 48 + .../wlxbase_wlqt/FilterableHeaderView.h | 61 + .../include/wlxbase_wlqt/FindReplacePanel.h | 74 + .../include/wlxbase_wlqt/FocusManager.h | 98 ++ .../include/wlxbase_wlqt/PluginSplitView.h | 26 + .../include/wlxbase_wlqt/PluginStatusBar.h | 40 + .../include/wlxbase_wlqt/PluginToolBar.h | 33 + .../wlxbase_wlqt/ScopedFindReplacePanel.h | 25 + .../wlxbase_wlqt/SequentialRowProxyModel.h | 16 + .../include/wlxbase_wlqt/ThemeManager.h | 29 + wlx/wlxbase_wlqt/src/CrashLogger.cpp | 193 +++ wlx/wlxbase_wlqt/src/EditableGridWidget.cpp | 1206 ++++++++++++++++ wlx/wlxbase_wlqt/src/EncodingUtils.cpp | 122 ++ wlx/wlxbase_wlqt/src/FilterRowWidget.cpp | 102 ++ wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp | 173 +++ wlx/wlxbase_wlqt/src/FindReplacePanel.cpp | 151 ++ wlx/wlxbase_wlqt/src/FocusManager.cpp | 332 +++++ wlx/wlxbase_wlqt/src/PluginSplitView.cpp | 34 + wlx/wlxbase_wlqt/src/PluginStatusBar.cpp | 136 ++ wlx/wlxbase_wlqt/src/PluginToolBar.cpp | 65 + .../src/ScopedFindReplacePanel.cpp | 34 + .../src/SequentialRowProxyModel.cpp | 22 + wlx/wlxbase_wlqt/src/ThemeManager.cpp | 260 ++++ 83 files changed, 17828 insertions(+) create mode 100644 wlx/dbview/.gitignore create mode 100644 wlx/dbview/CMakeLists.txt create mode 100644 wlx/dbview/README.md create mode 100644 wlx/dbview/dbview.png create mode 100644 wlx/dbview/include/BdbEngine.h create mode 100644 wlx/dbview/include/DbEngine.h create mode 100644 wlx/dbview/include/DbViewWidget.h create mode 100644 wlx/dbview/include/DuckDbEngine.h create mode 100644 wlx/dbview/include/DuckDbModel.h create mode 100644 wlx/dbview/include/FirebirdEngine.h create mode 100644 wlx/dbview/include/KeyValueModel.h create mode 100644 wlx/dbview/include/LevelDbEngine.h create mode 100644 wlx/dbview/include/LmdbEngine.h create mode 100644 wlx/dbview/include/MdbEngine.h create mode 100644 wlx/dbview/include/RocksDbEngine.h create mode 100644 wlx/dbview/include/SqliteEngine.h create mode 100644 wlx/dbview/include/mdbtools/mdbfakeglib.h create mode 100644 wlx/dbview/include/mdbtools/mdbprivate.h create mode 100644 wlx/dbview/include/mdbtools/mdbsql.h create mode 100644 wlx/dbview/include/mdbtools/mdbtools.h create mode 100644 wlx/dbview/include/mdbtools/mdbver.h create mode 100644 wlx/dbview/src/BdbEngine.cpp create mode 100644 wlx/dbview/src/DbEngine.cpp create mode 100644 wlx/dbview/src/DbViewWidget.cpp create mode 100644 wlx/dbview/src/DuckDbEngine.cpp create mode 100644 wlx/dbview/src/DuckDbModel.cpp create mode 100644 wlx/dbview/src/FirebirdEngine.cpp create mode 100644 wlx/dbview/src/KeyValueModel.cpp create mode 100644 wlx/dbview/src/LevelDbEngine.cpp create mode 100644 wlx/dbview/src/LmdbEngine.cpp create mode 100644 wlx/dbview/src/MdbEngine.cpp create mode 100644 wlx/dbview/src/RocksDbEngine.cpp create mode 100644 wlx/dbview/src/SqliteEngine.cpp create mode 100644 wlx/dbview/src/libmdb/backend.c create mode 100644 wlx/dbview/src/libmdb/catalog.c create mode 100644 wlx/dbview/src/libmdb/data.c create mode 100644 wlx/dbview/src/libmdb/dump.c create mode 100644 wlx/dbview/src/libmdb/fakeglib.c create mode 100644 wlx/dbview/src/libmdb/file.c create mode 100644 wlx/dbview/src/libmdb/iconv.c create mode 100644 wlx/dbview/src/libmdb/index.c create mode 100644 wlx/dbview/src/libmdb/like.c create mode 100644 wlx/dbview/src/libmdb/map.c create mode 100644 wlx/dbview/src/libmdb/money.c create mode 100644 wlx/dbview/src/libmdb/options.c create mode 100644 wlx/dbview/src/libmdb/props.c create mode 100644 wlx/dbview/src/libmdb/rc4.c create mode 100644 wlx/dbview/src/libmdb/sargs.c create mode 100644 wlx/dbview/src/libmdb/stats.c create mode 100644 wlx/dbview/src/libmdb/table.c create mode 100644 wlx/dbview/src/libmdb/version.c create mode 100644 wlx/dbview/src/libmdb/worktable.c create mode 100644 wlx/dbview/src/libmdb/write.c create mode 100644 wlx/dbview/src/wlx_entry.cpp create mode 100644 wlx/wlxbase_wlqt/CMakeLists.txt create mode 100644 wlx/wlxbase_wlqt/README.md create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/CrashLogger.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/EditableGridWidget.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/EncodingUtils.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterRowWidget.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterableHeaderView.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/FindReplacePanel.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/FocusManager.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginSplitView.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginStatusBar.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginToolBar.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/ScopedFindReplacePanel.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/SequentialRowProxyModel.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/ThemeManager.h create mode 100644 wlx/wlxbase_wlqt/src/CrashLogger.cpp create mode 100644 wlx/wlxbase_wlqt/src/EditableGridWidget.cpp create mode 100644 wlx/wlxbase_wlqt/src/EncodingUtils.cpp create mode 100644 wlx/wlxbase_wlqt/src/FilterRowWidget.cpp create mode 100644 wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp create mode 100644 wlx/wlxbase_wlqt/src/FindReplacePanel.cpp create mode 100644 wlx/wlxbase_wlqt/src/FocusManager.cpp create mode 100644 wlx/wlxbase_wlqt/src/PluginSplitView.cpp create mode 100644 wlx/wlxbase_wlqt/src/PluginStatusBar.cpp create mode 100644 wlx/wlxbase_wlqt/src/PluginToolBar.cpp create mode 100644 wlx/wlxbase_wlqt/src/ScopedFindReplacePanel.cpp create mode 100644 wlx/wlxbase_wlqt/src/SequentialRowProxyModel.cpp create mode 100644 wlx/wlxbase_wlqt/src/ThemeManager.cpp diff --git a/sdk/wlxplugin.h b/sdk/wlxplugin.h index 8f33fb2..eebce8e 100644 --- a/sdk/wlxplugin.h +++ b/sdk/wlxplugin.h @@ -9,6 +9,7 @@ #define lc_newparams 2 #define lc_selectall 3 #define lc_setpercent 4 +#define lc_focus 5 #define lcp_wraptext 1 #define lcp_fittowindow 2 diff --git a/wlx/dbview/.gitignore b/wlx/dbview/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/wlx/dbview/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/wlx/dbview/CMakeLists.txt b/wlx/dbview/CMakeLists.txt new file mode 100644 index 0000000..ded343c --- /dev/null +++ b/wlx/dbview/CMakeLists.txt @@ -0,0 +1,174 @@ +cmake_minimum_required(VERSION 3.20) +project(dbview_qt6 CXX C) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) + +# Allow older cmake_minimum_required in FetchContent dependencies +set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE) + +# Optional engine flags +option(ENABLE_ROCKSDB_LEVELDB "Enable RocksDB and LevelDB support using RocksDB (large dependency)" OFF) +if(ENABLE_ROCKSDB_LEVELDB) + add_compile_options(-include stdint.h) +endif() + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Sql) + +# Ensure the platform static library is built with -fPIC +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Pull in the platform library (builds libwlxbase_wlqt.a) +add_subdirectory( + ${CMAKE_CURRENT_SOURCE_DIR}/../wlxbase_wlqt + ${CMAKE_CURRENT_BINARY_DIR}/wlxbase_wlqt +) + +# --------------------------------------------------------------------------- +# External dependencies via FetchContent +# --------------------------------------------------------------------------- +include(FetchContent) + +# --- DuckDB --- +set(BUILD_SHELL OFF CACHE BOOL "" FORCE) +set(BUILD_UNITTESTS OFF CACHE BOOL "" FORCE) +set(BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) +set(BUILD_EXTENSIONS "parquet" CACHE STRING "" FORCE) +set(EXTENSION_STATIC_BUILD ON CACHE BOOL "" FORCE) +set(ENABLE_EXTENSION_AUTOLOADING OFF CACHE BOOL "" FORCE) +set(ENABLE_EXTENSION_AUTOINSTALL OFF CACHE BOOL "" FORCE) +FetchContent_Declare(duckdb + GIT_REPOSITORY https://github.com/duckdb/duckdb.git + GIT_TAG v1.3.0 + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(duckdb) + +# --- RocksDB (optional) --- +if(ENABLE_ROCKSDB_LEVELDB) + set(WITH_TESTS OFF CACHE BOOL "" FORCE) + set(WITH_BENCHMARK_TOOLS OFF CACHE BOOL "" FORCE) + set(WITH_TOOLS OFF CACHE BOOL "" FORCE) + set(WITH_GFLAGS OFF CACHE BOOL "" FORCE) + set(ROCKSDB_BUILD_SHARED OFF CACHE BOOL "" FORCE) + set(FAIL_ON_WARNINGS OFF CACHE BOOL "" FORCE) + FetchContent_Declare(rocksdb + GIT_REPOSITORY https://github.com/facebook/rocksdb.git + GIT_TAG v9.11.2 + GIT_SHALLOW TRUE + PATCH_COMMAND sed -i "1i #include " db/blob/blob_file_meta.h + ) + FetchContent_MakeAvailable(rocksdb) +endif() + +# --- MS Access static libmdb --- +add_library(mdb STATIC + src/libmdb/catalog.c + src/libmdb/file.c + src/libmdb/table.c + src/libmdb/data.c + src/libmdb/dump.c + src/libmdb/backend.c + src/libmdb/money.c + src/libmdb/sargs.c + src/libmdb/index.c + src/libmdb/like.c + src/libmdb/write.c + src/libmdb/stats.c + src/libmdb/map.c + src/libmdb/props.c + src/libmdb/worktable.c + src/libmdb/options.c + src/libmdb/iconv.c + src/libmdb/version.c + src/libmdb/rc4.c + src/libmdb/fakeglib.c +) +target_include_directories(mdb PRIVATE + include/mdbtools +) +target_compile_definitions(mdb PRIVATE FAKE_GLIB=1 "TLS=__thread" HAVE_ICONV=1 "ICONV_CONST=") + +# --------------------------------------------------------------------------- +# Plugin library +# --------------------------------------------------------------------------- +# NOTE: Headers must be listed explicitly so AUTOMOC processes them. +# RocksDbEngine.h is excluded when ENABLE_ROCKSDB is OFF to prevent MOC +# from generating code that references uncompiled RocksDbEngine symbols. +set(DBVIEW_HEADERS + include/DbEngine.h + include/DbViewWidget.h + include/DuckDbEngine.h + include/DuckDbModel.h + include/KeyValueModel.h + include/SqliteEngine.h + include/FirebirdEngine.h + include/MdbEngine.h + include/BdbEngine.h + include/LmdbEngine.h +) + +set(DBVIEW_SOURCES + ${DBVIEW_HEADERS} + src/wlx_entry.cpp + src/DbEngine.cpp + src/DbViewWidget.cpp + src/SqliteEngine.cpp + src/DuckDbEngine.cpp + src/DuckDbModel.cpp + src/KeyValueModel.cpp + src/FirebirdEngine.cpp + src/MdbEngine.cpp + src/BdbEngine.cpp + src/LmdbEngine.cpp +) + +if(ENABLE_ROCKSDB_LEVELDB) + list(APPEND DBVIEW_SOURCES + include/LevelDbEngine.h src/LevelDbEngine.cpp + include/RocksDbEngine.h src/RocksDbEngine.cpp + ) +endif() + +add_library(dbview_qt6 SHARED ${DBVIEW_SOURCES}) + +if(ENABLE_ROCKSDB_LEVELDB) + target_compile_definitions(dbview_qt6 PRIVATE ENABLE_ROCKSDB_LEVELDB) +endif() + +set_target_properties(dbview_qt6 PROPERTIES + PREFIX "" + SUFFIX ".wlx" +) + +target_include_directories(dbview_qt6 PRIVATE + include + include/mdbtools + ${CMAKE_CURRENT_SOURCE_DIR}/../../sdk + ${duckdb_SOURCE_DIR}/src/include +) + +if(ENABLE_ROCKSDB_LEVELDB) + target_include_directories(dbview_qt6 PRIVATE + ${rocksdb_SOURCE_DIR}/include + ) +endif() + +set(DBVIEW_LINK_LIBS + wlxbase_wlqt + Qt6::Widgets + Qt6::Gui + Qt6::Core + Qt6::Sql + duckdb + mdb + lmdb + db +) + +if(ENABLE_ROCKSDB_LEVELDB) + list(APPEND DBVIEW_LINK_LIBS rocksdb) +endif() + +target_link_libraries(dbview_qt6 PRIVATE ${DBVIEW_LINK_LIBS}) diff --git a/wlx/dbview/README.md b/wlx/dbview/README.md new file mode 100644 index 0000000..499146c --- /dev/null +++ b/wlx/dbview/README.md @@ -0,0 +1,120 @@ +# dbview — Multi-Engine Database WLX Plugin + +![dbview screenshot](dbview.png) + +A Qt6 WLX (Lister) plugin for [Double Commander](https://doublecmd.github.io/) that views and edits database files: **SQLite**, **DuckDB**, **LevelDB**, **RocksDB**, **LMDB**, **Berkeley DB**, **Firebird Embedded**, **MS Access**, and **Apache Parquet**. + +Built on the [`wlxbase_wlqt`](../wlxbase_wlqt/) platform library. + +> [!WARNING] +> **DATA MUTATION & LOCKING WARNING** +> +> By default, this plugin attempts to open databases in **read-write** mode to allow direct grid editing and data mutation. +> - **File Locking:** Opening a database with read-write privileges may lock the file, preventing other applications from writing to it. +> - **Concurrent Access Fallback:** If the database file is locked by another process, the plugin will silently fall back to **read-only** mode. +> - **Data Integrity:** Any edited cells must be explicitly committed using the **Commit** button (or `Ctrl+S` / `Ctrl+Shift+Z`) for relational databases, or are saved instantly for key-value stores. Handle write mode with care to prevent unintended database modifications. + +--- + +## Features + +### All Engines +- **Schema Navigation Tree:** Hierarchical tree panel on the left displaying Tables, Views, Columns (with data types, Primary/Foreign keys), and Indexes. +- **Find** (`Ctrl+F`) — search across all visible cells in the selected table. +- **Copy Selection** (`Ctrl+C`) — copy selected cell values as tab-separated values. +- **Word Wrap & Grid Lines** — toolbar actions to toggle wrapping and gridlines. +- **`GridMode::LiveDatabase`** — direct table mapping with minimal memory overhead. + +### SQL Engines (SQLite, DuckDB, Firebird Embedded, Apache Parquet) +- **SQL Console:** A vertical split panel containing a query editor (with execution via `Ctrl+Return` or `Execute` button), results grid, and CSV/TSV results exporter. +- **Apache Parquet Proxying:** Opening a `.parquet`/`.pq` file initializes an in-memory DuckDB database and reads it via a virtual `read_parquet` view, making it SQL-queryable. +- **In-place Grid Editing:** Cells are editable, with modifications buffered. +- **Commit / Revert** (`Ctrl+S` / `Ctrl+Z` or `Ctrl+Shift+Z`) — commit or rollback pending changes. + +### Key-Value & Non-Relational Engines (LevelDB, RocksDB, LMDB, Berkeley DB, MS Access) +- **Hidden SQL Console:** The SQL Console panel is automatically hidden as these engines do not support custom SQL. +- **Two-Column Grid:** Displays Key and Value columns. +- **Immediate Writing:** Key-value writes are saved instantly (no buffering). +- **Binary/BLOB Value Detection:** Non-UTF-8 values and large binaries display placeholder information `[Binary Data - X bytes]`. +- **Right-Click Context Menu Options:** + - **Hex View Toggle:** Displays binary data as space-separated hex strings. + - **BLOB Export ("Save Cell to File"):** Save raw binary values to any local file. + - **BLOB Import ("Load File into Cell"):** Import binary files into a cell (available in write mode only). +- **Directory Detection (LevelDB/RocksDB):** Selecting a `.sst`/`.ldb`/`.log` file inside a LevelDB/RocksDB directory automatically targets the parent database directory. + +--- + +## Keyboard Shortcuts + +| Shortcut | Action | +|----------|--------| +| `Ctrl+S` | Commit changes (Relational engines only) | +| `Ctrl+Z` | Revert pending changes (Relational engines only) | +| `Ctrl+Shift+Z` | Alternative Commit/Redo shortcut | +| `Ctrl+Return` | Execute custom SQL query inside the SQL Console | +| `Ctrl+F` | Toggle Find panel | +| `Ctrl+C` | Copy selection | + +--- + +## Engine Selection Logic + +The factory (`DbEngine::createForFile()`) resolves the file type by extension and locks: + +| Extension / File Pattern | Engine | SQL Console | Read-Only | Notes | +|-------------------------|--------|-------------|-----------|-------| +| `.sqlite`, `.sqlite3`, `.db`, `.db3` | SQLite | Yes | No* | QSQLITE driver connection | +| `.duckdb` | DuckDB | Yes | No* | C++ native client API | +| `.parquet`, `.pq` | DuckDB | Yes | No* | Virtual view via `read_parquet()` | +| `.fdb` | Firebird Embedded | Yes | No* | QIBASE SQL driver connection | +| `.mdb`, `.accdb` (if user table) | MS Access | No | **Yes** | Statically linked `libmdb` | +| `.lmdb`, `data.mdb` | LMDB | No | No* | C API client | +| `.bdb` | Berkeley DB | No | No* | C API with B-Tree cursors | +| `.ldb`, `.sst`, `.log` (if parent has `CURRENT`) | LevelDB | No | **Yes** | C++ API client via RocksDB (if compiled) | +| `.sst`, `.log` (fallback if LevelDB fails) | RocksDB | No | **Yes** | C++ API (if compiled) | + +*\* Note: Opens as Read-Only automatically if the file lacks write permissions or if the database is currently locked by another process.* + +--- + +## Building + +### Prerequisites +- Qt6 (Core, Gui, Widgets, Sql) +- CMake ≥ 3.20 +- Git (for FetchContent dependencies) +- Libraries: `liblmdb`, `libdb` (Berkeley DB), `libfbclient` (Firebird client) + +### Compile +```bash +cd wlx/dbview +mkdir build && cd build +cmake .. +make -j$(nproc) +``` + +To enable support for **LevelDB** and **RocksDB**, you must compile with the `ENABLE_ROCKSDB_LEVELDB` flag set to `ON`. This flag is disabled by default because linking the RocksDB libraries increases the final `dbview_qt6.wlx` plugin size by over 12 MB. Both formats will be opened in strict read-only mode. + +```bash +cmake .. -DENABLE_ROCKSDB_LEVELDB=ON +make -j$(nproc) +``` + +**Output:** `dbview_qt6.wlx` (shared library) + +--- + +## Installation + +Add `dbview_qt6.wlx` to your Double Commander plugins list. + +**Default Detection String:** +``` +EXT="DB" | EXT="SQLITE" | EXT="SQLITE3" | EXT="DB3" | EXT="DUCKDB" | EXT="LMDB" | EXT="BDB" | EXT="FDB" | EXT="MDB" | EXT="ACCDB" | EXT="PARQUET" | EXT="PQ" +``` + +**If compiled with `ENABLE_ROCKSDB_LEVELDB=ON`:** +Include the extensions for LevelDB and RocksDB: +``` +EXT="DB" | EXT="SQLITE" | EXT="SQLITE3" | EXT="DB3" | EXT="DUCKDB" | EXT="LDB" | EXT="SST" | EXT="LOG" | EXT="LMDB" | EXT="BDB" | EXT="FDB" | EXT="MDB" | EXT="ACCDB" | EXT="PARQUET" | EXT="PQ" +``` diff --git a/wlx/dbview/dbview.png b/wlx/dbview/dbview.png new file mode 100644 index 0000000000000000000000000000000000000000..0a4daebce6923b08cc744d5a08f9b260313818f0 GIT binary patch literal 486442 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

EjlmbTBb&GXB*PRUS#=|7Xh&CrtL5wyg|Hg>! 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

~~(E<{qY+heed&_Gu(OQqKfKzSwC3}b_yr`L2Z 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 + +class BdbEngine : public DbEngine { + Q_OBJECT +public: + explicit BdbEngine(QObject *parent = nullptr); + ~BdbEngine() override; + + bool open(const QString &filepath) override; + void close() override; + + QStringList tableNames() const override; + QAbstractItemModel *modelForTable(const QString &tableName) override; + QString currentTableName() const override; + + bool supportsMultipleTables() const override { return false; } + bool supportsSubmitRevert() const override { return false; } + QString engineName() const override { return QStringLiteral("Berkeley DB"); } + +private: + int countKeys() const; + + DB *m_db = nullptr; + int m_keyCount = 0; + QAbstractItemModel *m_model = nullptr; +}; diff --git a/wlx/dbview/include/DbEngine.h b/wlx/dbview/include/DbEngine.h new file mode 100644 index 0000000..87ac26e --- /dev/null +++ b/wlx/dbview/include/DbEngine.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include + +class QAbstractItemModel; + +struct ColumnInfo { + QString name; + QString type; + bool isPrimaryKey = false; + bool isForeignKey = false; +}; + +/// Abstract backend interface for all database engines. +/// +/// Provides a polymorphic API that DbViewWidget programs against, +/// decoupling the UI from any specific database technology. +/// +/// SQL engines (SQLite, DuckDB) return QSqlTableModel or custom models +/// with multiple tables. KV engines (LevelDB, RocksDB) return a two-column +/// KeyValueModel with a single "table". +class DbEngine : public QObject { + Q_OBJECT +public: + explicit DbEngine(QObject *parent = nullptr) : QObject(parent), m_readOnly(false) {} + ~DbEngine() override = default; + + /// Open a database file (or directory for KV stores). + virtual bool open(const QString &filepath) = 0; + + /// Close the database and release resources. + virtual void close() = 0; + + /// List of table/keyspace names. KV stores return {""}. + virtual QStringList tableNames() const = 0; + + /// List of view names (SQL engines only). + virtual QStringList viewNames() const { return {}; } + + /// Get column metadata for a table/view. + virtual QList columnInfos(const QString &tableName) const { + Q_UNUSED(tableName); + return {}; + } + + /// Get index names for a table. + virtual QStringList indexes(const QString &tableName) const { + Q_UNUSED(tableName); + return {}; + } + + /// Get a model for the given table. Ownership stays with the engine. + /// The model is replaced when a different table is selected. + virtual QAbstractItemModel *modelForTable(const QString &tableName) = 0; + + /// Currently selected table name. + virtual QString currentTableName() const = 0; + + /// Whether the engine has multiple switchable tables. + virtual bool supportsMultipleTables() const = 0; + + /// Whether the engine buffers writes until explicit submit. + /// SQL engines: true (OnManualSubmit). KV engines: false (direct writes). + virtual bool supportsSubmitRevert() const = 0; + + /// Human-readable engine name for display in the toolbar. + virtual QString engineName() const = 0; + + /// Submit buffered writes to the database. SQL engines only. + virtual bool submitAll() { return false; } + + /// Revert all pending changes. SQL engines only. + virtual bool revertAll() { return false; } + + /// Execute a custom SQL query (SQL engines only). + virtual QAbstractItemModel *executeQuery(const QString &query) { + Q_UNUSED(query); + return nullptr; + } + + /// Whether the engine supports the SQL console. + virtual bool supportsSqlConsole() const { return false; } + + /// Whether the last custom query execution failed. + virtual bool lastQueryError() const { return false; } + + /// Last error message from the engine. + virtual QString lastError() const { return {}; } + + /// Accessors for read-only mode. + bool isReadOnly() const { return m_readOnly; } + void setReadOnly(bool ro) { m_readOnly = ro; } + + /// Factory: pick the right engine based on file extension / content. + static std::unique_ptr createForFile(const QString &filepath); + +protected: + bool m_readOnly; +}; diff --git a/wlx/dbview/include/DbViewWidget.h b/wlx/dbview/include/DbViewWidget.h new file mode 100644 index 0000000..8ded36c --- /dev/null +++ b/wlx/dbview/include/DbViewWidget.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class QAction; +class QStandardItem; + +namespace QtWlPlugin { +class FocusManager; +class PluginToolBar; +class EditableGridWidget; +class FindReplacePanel; +class FilterableHeaderView; +class PluginStatusBar; +class PluginSplitView; +} + +class DbEngine; +class KeyValueModel; +class QSortFilterProxyModel; + +/// Main plugin widget for database file viewing/editing. +class DbViewWidget : public QWidget { + Q_OBJECT +public: + explicit DbViewWidget(QWidget *parent = nullptr); + ~DbViewWidget() override; + + bool loadFile(const QString &filepath); + + // WLX bridge accessors + QtWlPlugin::FocusManager *focusManager() const; + QtWlPlugin::EditableGridWidget *grid() const; + QString getSelectionAsText(char sep = '\t'); + +private slots: + void onSubmitChanges(); + void onRevertChanges(); + void onFind(bool forward); + void onExecuteSqlQuery(); + void onClearSqlConsole(); + void onExportSqlResults(); + void onExportTableData(); + +private: + void setupUi(const QString &firstTable); + void setupToolbar(); + void setupFindReplace(); + void rebuildGrid(const QString &tableName); + void updateStatusBar(); + void populateSchemaTree(); + void setupContextMenu(); + + // BLOB helpers + bool isCellBinary(const QModelIndex &idx) const; + QByteArray getCellRawValue(const QModelIndex &idx) const; + bool setCellRawValue(const QModelIndex &idx, const QByteArray &bytes); + + bool eventFilter(QObject *obj, QEvent *event) override; + + std::unique_ptr m_engine; + QtWlPlugin::FocusManager *m_fm = nullptr; + QtWlPlugin::PluginToolBar *m_toolbar = nullptr; + QtWlPlugin::EditableGridWidget *m_grid = nullptr; + QtWlPlugin::FindReplacePanel *m_findPanel = nullptr; + QtWlPlugin::FilterableHeaderView *m_filterHeader = nullptr; + QtWlPlugin::PluginStatusBar *m_statusBar = nullptr; + + QTableView *m_tableView = nullptr; + QTreeView *m_schemaTree = nullptr; + QAction *m_actSubmit = nullptr; + QAction *m_actRevert = nullptr; + QSortFilterProxyModel *m_filterProxy = nullptr; + + // SQL Console + QWidget *m_sqlConsoleWidget = nullptr; + QPlainTextEdit *m_sqlEditor = nullptr; + QStackedWidget *m_sqlResultsStack = nullptr; + QTableView *m_sqlResultsView = nullptr; + QTextEdit *m_sqlMessageView = nullptr; + QSplitter *m_mainSplitter = nullptr; +}; diff --git a/wlx/dbview/include/DuckDbEngine.h b/wlx/dbview/include/DuckDbEngine.h new file mode 100644 index 0000000..b113dd0 --- /dev/null +++ b/wlx/dbview/include/DuckDbEngine.h @@ -0,0 +1,53 @@ +#pragma once + +#include "DbEngine.h" +#include + +namespace duckdb { +class DuckDB; +class Connection; +} + +/// DuckDB engine: opens DuckDB database files via the native C++ API. +/// +/// Uses a custom DuckDbModel (QAbstractTableModel) instead of QSqlTableModel +/// since there's no Qt SQL driver for DuckDB. +/// +/// Supports multiple tables, submit/revert via BEGIN/COMMIT/ROLLBACK. +class DuckDbEngine : public DbEngine { + Q_OBJECT +public: + explicit DuckDbEngine(QObject *parent = nullptr); + ~DuckDbEngine() override; + + bool open(const QString &filepath) override; + void close() override; + + QStringList tableNames() const override; + QStringList viewNames() const override; + QList columnInfos(const QString &tableName) const override; + QStringList indexes(const QString &tableName) const override; + + QAbstractItemModel *modelForTable(const QString &tableName) override; + QString currentTableName() const override; + + bool supportsMultipleTables() const override { return true; } + bool supportsSubmitRevert() const override { return true; } + bool supportsSqlConsole() const override { return true; } + bool lastQueryError() const override { return m_lastQueryError; } + QString engineName() const override { return QStringLiteral("DuckDB"); } + + bool submitAll() override; + bool revertAll() override; + QAbstractItemModel *executeQuery(const QString &query) override; + QString lastError() const override; + +private: + std::unique_ptr m_duckdb; + std::unique_ptr m_conn; + QString m_currentTable; + QString m_lastError; + QAbstractItemModel *m_currentModel = nullptr; + bool m_inTransaction = false; + bool m_lastQueryError = false; +}; diff --git a/wlx/dbview/include/DuckDbModel.h b/wlx/dbview/include/DuckDbModel.h new file mode 100644 index 0000000..54c46ad --- /dev/null +++ b/wlx/dbview/include/DuckDbModel.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +namespace duckdb { +class Connection; +} + +/// Custom QAbstractTableModel for DuckDB query results. +/// +/// Loads data in chunks using LIMIT/OFFSET for lazy loading. +/// Supports editing via UPDATE statements. +class DuckDbModel : public QAbstractTableModel { + Q_OBJECT +public: + explicit DuckDbModel(duckdb::Connection *conn, const QString &tableNameOrQuery, + QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + void fetchMore(const QModelIndex &parent) override; + bool canFetchMore(const QModelIndex &parent) const override; + + void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; + + bool select(); + + // BLOB helpers + bool isBinaryValue(int row, int col) const; + QByteArray rawValue(int row, int col) const; + +private: + void loadChunk(int offset, int limit); + + duckdb::Connection *m_conn; + QString m_tableName; + QString m_query; + bool m_isQuery = false; + QStringList m_columnNames; + QVector> m_data; + int m_totalRows = 0; + bool m_allFetched = false; + int m_sortColumn = -1; + Qt::SortOrder m_sortOrder = Qt::AscendingOrder; + bool m_hasRowId = false; + QVector m_rowIds; + static constexpr int kChunkSize = 1000; +}; diff --git a/wlx/dbview/include/FirebirdEngine.h b/wlx/dbview/include/FirebirdEngine.h new file mode 100644 index 0000000..994864c --- /dev/null +++ b/wlx/dbview/include/FirebirdEngine.h @@ -0,0 +1,44 @@ +#pragma once + +#include "DbEngine.h" +#include +#include + +class QSqlTableModel; + +class FirebirdEngine : public DbEngine { + Q_OBJECT +public: + explicit FirebirdEngine(QObject *parent = nullptr); + ~FirebirdEngine() override; + + bool open(const QString &filepath) override; + void close() override; + + QStringList tableNames() const override; + QStringList viewNames() const override; + QList columnInfos(const QString &tableName) const override; + QStringList indexes(const QString &tableName) const override; + + QAbstractItemModel *modelForTable(const QString &tableName) override; + QString currentTableName() const override; + + bool supportsMultipleTables() const override { return true; } + bool supportsSubmitRevert() const override { return true; } + bool supportsSqlConsole() const override { return true; } + bool lastQueryError() const override { return m_lastQueryError; } + QString engineName() const override { return QStringLiteral("Firebird Embedded"); } + + bool submitAll() override; + bool revertAll() override; + QAbstractItemModel *executeQuery(const QString &query) override; + QString lastError() const override; + +private: + QString m_connectionName; + QSqlDatabase m_db; + QString m_currentTable; + QPointer m_currentModel; + bool m_lastQueryError = false; + QString m_lastError; +}; diff --git a/wlx/dbview/include/KeyValueModel.h b/wlx/dbview/include/KeyValueModel.h new file mode 100644 index 0000000..287c25d --- /dev/null +++ b/wlx/dbview/include/KeyValueModel.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include + +/// QAbstractTableModel for Key-Value stores (LevelDB, RocksDB). +/// +/// Uses a sliding window cache of ~1000 entries centered on the viewport. +/// Binary values that fail UTF-8 validation are displayed as +/// "[Binary Data - X bytes]" with support for hex view toggle +/// and save/load binary values as files. +/// +/// This model works with abstract iterator callbacks so the same model +/// serves both LevelDB and RocksDB. +class KeyValueModel : public QAbstractTableModel { + Q_OBJECT +public: + struct Entry { + QByteArray key; + QByteArray value; + }; + + /// Iterator abstraction: the engine provides callbacks for iteration. + struct IteratorOps { + /// Fetch entries for the window [startIndex, startIndex + count). + std::function(int startIndex, int count)> fetchWindow; + + /// Write a value for the given key. Returns true on success. + std::function putValue; + + /// Delete a key. Returns true on success. + std::function deleteKey; + }; + + explicit KeyValueModel(int totalRows, IteratorOps ops, QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + /// Check if a value at a given row is binary (non-UTF-8). + bool isBinaryValue(int row) const; + + /// Get raw binary value for a row (for save-to-file). + QByteArray rawValue(int row) const; + + /// Get raw key for a row. + QByteArray rawKey(int row) const; + + /// Set hex display mode for a row. + void setHexMode(int row, bool enabled); + + /// Replace a value from file contents. + bool loadValueFromFile(int row, const QByteArray &fileData); + +private: + void ensureCached(int row) const; + static bool isValidUtf8(const QByteArray &data); + static QString formatBinaryPlaceholder(int size); + static QString toHexString(const QByteArray &data); + + int m_totalRows; + IteratorOps m_ops; + + // Sliding window cache + static constexpr int kWindowSize = 1000; + mutable QVector m_cache; + mutable int m_cacheStartRow = -1; + + // Per-row hex display toggle + mutable QSet m_hexRows; +}; diff --git a/wlx/dbview/include/LevelDbEngine.h b/wlx/dbview/include/LevelDbEngine.h new file mode 100644 index 0000000..9173623 --- /dev/null +++ b/wlx/dbview/include/LevelDbEngine.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef ENABLE_ROCKSDB_LEVELDB + +#include "DbEngine.h" +#include + +namespace rocksdb { class DB; } + +/// LevelDB engine: opens LevelDB directories as Key-Value stores using RocksDB API. +class LevelDbEngine : public DbEngine { + Q_OBJECT +public: + explicit LevelDbEngine(QObject *parent = nullptr); + ~LevelDbEngine() override; + + bool open(const QString &filepath) override; + void close() override; + + QStringList tableNames() const override; + QAbstractItemModel *modelForTable(const QString &tableName) override; + QString currentTableName() const override; + + bool supportsMultipleTables() const override { return false; } + bool supportsSubmitRevert() const override { return false; } + QString engineName() const override { return QStringLiteral("LevelDB"); } + +private: + int countKeys() const; + + rocksdb::DB *m_db = nullptr; + int m_keyCount = 0; + QAbstractItemModel *m_model = nullptr; +}; + +#endif // ENABLE_ROCKSDB_LEVELDB diff --git a/wlx/dbview/include/LmdbEngine.h b/wlx/dbview/include/LmdbEngine.h new file mode 100644 index 0000000..ecf3a0c --- /dev/null +++ b/wlx/dbview/include/LmdbEngine.h @@ -0,0 +1,32 @@ +#pragma once + +#include "DbEngine.h" +#include +#undef mdb_open +#undef mdb_close + +class LmdbEngine : public DbEngine { + Q_OBJECT +public: + explicit LmdbEngine(QObject *parent = nullptr); + ~LmdbEngine() override; + + bool open(const QString &filepath) override; + void close() override; + + QStringList tableNames() const override; + QAbstractItemModel *modelForTable(const QString &tableName) override; + QString currentTableName() const override; + + bool supportsMultipleTables() const override { return false; } + bool supportsSubmitRevert() const override { return false; } + QString engineName() const override { return QStringLiteral("LMDB"); } + +private: + int countKeys() const; + + MDB_env *m_env = nullptr; + MDB_dbi m_dbi = 0; + int m_keyCount = 0; + QAbstractItemModel *m_model = nullptr; +}; diff --git a/wlx/dbview/include/MdbEngine.h b/wlx/dbview/include/MdbEngine.h new file mode 100644 index 0000000..66554e6 --- /dev/null +++ b/wlx/dbview/include/MdbEngine.h @@ -0,0 +1,67 @@ +#pragma once + +#include "DbEngine.h" + +extern "C" { +#include +} + +#include +#include + +class MdbEngine; + +class MdbModel : public QAbstractTableModel { + Q_OBJECT +public: + MdbModel(MdbEngine *engine, MdbTableDef *table, QObject *parent = nullptr); + ~MdbModel() override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + // BLOB helpers + bool isBinaryValue(int row, int col) const; + QByteArray rawValue(int row, int col) const; + + bool select(); + +private: + MdbEngine *m_engine; + MdbTableDef *m_table; + QStringList m_columnNames; + QList m_columnTypes; + QVector> m_data; + QVector> m_rawData; // to preserve raw BLOB bytes +}; + +class MdbEngine : public DbEngine { + Q_OBJECT +public: + explicit MdbEngine(QObject *parent = nullptr); + ~MdbEngine() override; + + bool open(const QString &filepath) override; + void close() override; + + QStringList tableNames() const override; + QList columnInfos(const QString &tableName) const override; + QStringList indexes(const QString &tableName) const override; + + QAbstractItemModel *modelForTable(const QString &tableName) override; + QString currentTableName() const override; + + bool supportsMultipleTables() const override { return true; } + bool supportsSubmitRevert() const override { return false; } + QString engineName() const override { return QStringLiteral("MS Access"); } + + MdbHandle *handle() const { return m_mdb; } + +private: + MdbHandle *m_mdb = nullptr; + QString m_currentTable; + MdbModel *m_currentModel = nullptr; +}; diff --git a/wlx/dbview/include/RocksDbEngine.h b/wlx/dbview/include/RocksDbEngine.h new file mode 100644 index 0000000..8aae749 --- /dev/null +++ b/wlx/dbview/include/RocksDbEngine.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef ENABLE_ROCKSDB_LEVELDB + +#include "DbEngine.h" +#include + +namespace rocksdb { class DB; } + +/// RocksDB engine: opens RocksDB directories as Key-Value stores. +class RocksDbEngine : public DbEngine { + Q_OBJECT +public: + explicit RocksDbEngine(QObject *parent = nullptr); + ~RocksDbEngine() override; + + bool open(const QString &filepath) override; + void close() override; + + QStringList tableNames() const override; + QAbstractItemModel *modelForTable(const QString &tableName) override; + QString currentTableName() const override; + + bool supportsMultipleTables() const override { return false; } + bool supportsSubmitRevert() const override { return false; } + QString engineName() const override { return QStringLiteral("RocksDB"); } + +private: + int countKeys() const; + + rocksdb::DB *m_db = nullptr; + int m_keyCount = 0; + QAbstractItemModel *m_model = nullptr; +}; + +#endif // ENABLE_ROCKSDB_LEVELDB diff --git a/wlx/dbview/include/SqliteEngine.h b/wlx/dbview/include/SqliteEngine.h new file mode 100644 index 0000000..834c4b2 --- /dev/null +++ b/wlx/dbview/include/SqliteEngine.h @@ -0,0 +1,50 @@ +#pragma once + +#include "DbEngine.h" +#include + +class QSqlTableModel; + +/// SQLite engine: wraps Qt's QSQLITE driver via QSqlTableModel. +/// +/// Uses OnManualSubmit editing strategy so changes are buffered +/// until explicit submitAll() (wired to Ctrl+S in the toolbar). +/// +/// Each instance creates a unique QSqlDatabase connection name +/// via QUuid to avoid collisions when multiple plugin windows are open. +class SqliteEngine : public DbEngine { + Q_OBJECT +public: + explicit SqliteEngine(QObject *parent = nullptr); + ~SqliteEngine() override; + + bool open(const QString &filepath) override; + void close() override; + + QStringList tableNames() const override; + QStringList viewNames() const override; + QList columnInfos(const QString &tableName) const override; + QStringList indexes(const QString &tableName) const override; + + QAbstractItemModel *modelForTable(const QString &tableName) override; + QString currentTableName() const override; + + bool supportsMultipleTables() const override { return true; } + bool supportsSubmitRevert() const override { return true; } + bool supportsSqlConsole() const override { return true; } + bool lastQueryError() const override { return m_lastQueryError; } + QString engineName() const override { return QStringLiteral("SQLite"); } + + bool submitAll() override; + bool revertAll() override; + QAbstractItemModel *executeQuery(const QString &query) override; + QString lastError() const override; + +private: + QSqlDatabase m_db; + QString m_connectionName; + QString m_currentTable; + QSqlTableModel *m_currentModel = nullptr; + bool m_lastQueryError = false; + QString m_lastError; +}; diff --git a/wlx/dbview/include/mdbtools/mdbfakeglib.h b/wlx/dbview/include/mdbtools/mdbfakeglib.h new file mode 100644 index 0000000..daf93c1 --- /dev/null +++ b/wlx/dbview/include/mdbtools/mdbfakeglib.h @@ -0,0 +1,193 @@ +/* fakeglib.c - A shim for applications that require GLib + * without the whole kit and kaboodle. + * + * Copyright (C) 2020 Evan Miller + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _mdbfakeglib_h_ +#define _mdbfakeglib_h_ + +#include +#include +#include +#include + +typedef uint16_t guint16; +typedef uint32_t guint32; +typedef uint64_t guint64; +typedef int32_t gint32; +typedef char gchar; +typedef int gboolean; +typedef int gint; +typedef unsigned int guint; +typedef void * gpointer; +typedef const void * gconstpointer; +typedef uint8_t guint8; +typedef guint32 GQuark; +typedef guint32 gunichar; +typedef signed long gssize; + +typedef guint (*GHashFunc)(gconstpointer); +typedef int (*GCompareFunc)(gconstpointer, gconstpointer); +typedef gboolean (*GEqualFunc)(gconstpointer, gconstpointer); +typedef void (*GFunc) (gpointer data, gpointer user_data); +typedef void (*GHFunc)(gpointer key, gpointer value, gpointer data); +typedef gboolean (*GHRFunc)(gpointer key, gpointer value, gpointer data); + +typedef struct GString { + gchar *str; + size_t len; + size_t allocated_len; +} GString; + +typedef struct GPtrArray { + void **pdata; + guint len; +} GPtrArray; + +typedef struct GList { + gpointer data; + struct GList *next; + struct GList *prev; +} GList; + +typedef struct GHashTable { + GEqualFunc compare; + GPtrArray *array; +} GHashTable; + +typedef struct GError { + GQuark domain; + gint code; + gchar *message; +} GError; + +typedef enum GOptionArg { + G_OPTION_ARG_NONE, + G_OPTION_ARG_STRING, + G_OPTION_ARG_INT, + G_OPTION_ARG_CALLBACK, + G_OPTION_ARG_FILENAME +} GOptionArg; + +typedef enum GOptionFlags { + G_OPTION_FLAG_NONE, + G_OPTION_FLAG_REVERSE +} GOptionFlags; + +typedef struct GOptionEntry { + const gchar *long_name; + gchar short_name; + gint flags; + + GOptionArg arg; + gpointer arg_data; + + const gchar *description; + const gchar *arg_description; +} GOptionEntry; + +typedef struct GOptionContext { + const char *desc; + const GOptionEntry *entries; +} GOptionContext; + +#define g_str_hash NULL + +#define G_GUINT32_FORMAT PRIu32 + +#define g_return_val_if_fail(a, b) if (!a) { return b; } + +#define g_ascii_strcasecmp strcasecmp +#define g_malloc0(len) calloc(1, len) +#define g_malloc malloc +#define g_free free +#define g_realloc realloc +#define g_memdup2 g_memdup + +#define G_STR_DELIMITERS "_-|> <." + +#define g_ptr_array_index(array, i) \ + ((void **)array->pdata)[i] + +#define TRUE 1 +#define FALSE 0 + +#define GUINT32_SWAP_LE_BE(l) __builtin_bswap32((uint32_t)(l)) + +/* string functions */ +void *g_memdup(const void *src, size_t len); +int g_str_equal(const void *str1, const void *str2); +char **g_strsplit(const char *haystack, const char *needle, int max_tokens); +void g_strfreev(char **dir); +char *g_strconcat(const char *first, ...); +char *g_strdup(const char *src); +char *g_strndup(const char *src, size_t len); +char *g_strdup_printf(const char *format, ...); +gchar *g_strdelimit(gchar *string, const gchar *delimiters, gchar new_delimiter); +void g_printerr(const gchar *format, ...); + +/* conversion */ +gint g_unichar_to_utf8(gunichar c, gchar *dst); +gchar *g_locale_to_utf8(const gchar *opsysstring, size_t len, + size_t *bytes_read, size_t *bytes_written, GError **error); +gchar *g_utf8_casefold(const gchar *str, gssize len); +gchar *g_utf8_strdown(const gchar *str, gssize len); + +/* GString */ +GString *g_string_new(const gchar *init); +GString *g_string_assign(GString *string, const gchar *rval); +GString * g_string_append (GString *string, const gchar *val); +gchar *g_string_free (GString *string, gboolean free_segment); + +/* GHashTable */ +void *g_hash_table_lookup(GHashTable *tree, const void *key); +gboolean g_hash_table_lookup_extended(GHashTable *table, const void *lookup_key, + void **orig_key, void **value); +void g_hash_table_insert(GHashTable *tree, void *key, void *value); +gboolean g_hash_table_remove(GHashTable *hash_table, const void *key); +GHashTable *g_hash_table_new(GHashFunc hashes, GEqualFunc equals); +void g_hash_table_foreach(GHashTable *tree, GHFunc function, void *data); +void g_hash_table_foreach_remove(GHashTable *tree, GHRFunc function, void *data); +void g_hash_table_destroy(GHashTable *tree); + +/* GPtrArray */ +void g_ptr_array_sort(GPtrArray *array, GCompareFunc func); +void g_ptr_array_foreach(GPtrArray *array, GFunc function, gpointer user_data); +GPtrArray *g_ptr_array_new(void); +void g_ptr_array_add(GPtrArray *array, void *entry); +gboolean g_ptr_array_remove (GPtrArray *array, gpointer data); +void g_ptr_array_free(GPtrArray *array, gboolean something); + +/* GList */ +GList *g_list_append(GList *list, void *data); +GList *g_list_last(GList *list); +GList *g_list_remove(GList *list, void *data); +void g_list_free(GList *list); + +/* GOption */ +GOptionContext *g_option_context_new(const char *description); +void g_option_context_add_main_entries (GOptionContext *context, + const GOptionEntry *entries, + const gchar *translation_domain); +gchar *g_option_context_get_help (GOptionContext *context, + gboolean main_help, void *group); +gboolean g_option_context_parse (GOptionContext *context, + gint *argc, gchar ***argv, GError **error); +void g_option_context_free (GOptionContext *context); + +#endif diff --git a/wlx/dbview/include/mdbtools/mdbprivate.h b/wlx/dbview/include/mdbtools/mdbprivate.h new file mode 100644 index 0000000..b793e9a --- /dev/null +++ b/wlx/dbview/include/mdbtools/mdbprivate.h @@ -0,0 +1,51 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef MDBPRIVATE_H +#define MDBPRIVATE_H + +#include "mdbtools.h" + +/* + * This header is for stuff lacking a MDB_ or mdb_ something, or functions only + * used within mdbtools so they won't be exported to calling programs. + */ + +#ifdef __cplusplus + extern "C" { +#endif + +void mdbi_rc4(unsigned char *key, guint32 key_len, unsigned char *buf, guint32 buf_len); +MdbBackend *mdbi_register_backend2(MdbHandle *mdb, char *backend_name, guint32 capabilities, + const MdbBackendType *backend_type, + const MdbBackendType *type_shortdate, + const MdbBackendType *type_autonum, + const char *short_now, const char *long_now, + const char *date_fmt, const char *shortdate_fmt, + const char *charset_statement, const char *create_table_statement, + const char *drop_statement, const char *constaint_not_empty_statement, + const char *column_comment_statement, const char *per_column_comment_statement, + const char *table_comment_statement, const char *per_table_comment_statement, + gchar* (*quote_schema_name)(const gchar*, const gchar*), + gchar* (*normalise_case)(const gchar*)); + +#ifdef __cplusplus + } +#endif + +#endif diff --git a/wlx/dbview/include/mdbtools/mdbsql.h b/wlx/dbview/include/mdbtools/mdbsql.h new file mode 100644 index 0000000..4225a81 --- /dev/null +++ b/wlx/dbview/include/mdbtools/mdbsql.h @@ -0,0 +1,112 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _mdbsql_h_ +#define _mdbsql_h_ + +#ifdef __cplusplus + extern "C" { +#endif + +#include +#include +#include + +typedef struct MdbSQL +{ + MdbHandle *mdb; + int all_columns; + int sel_count; + unsigned int num_columns; + GPtrArray *columns; + unsigned int num_tables; + GPtrArray *tables; + MdbTableDef *cur_table; + MdbSargNode *sarg_tree; + GList *sarg_stack; + GPtrArray *bound_values; + unsigned char *kludge_ttable_pg; + long max_rows; + char error_msg[1024]; + int limit; + int limit_percent; + long row_count; +} MdbSQL; + +typedef struct { + char *name; + int disp_size; + void *bind_addr; /* if !NULL then cp parameter to here */ + int bind_type; + int *bind_len; + int bind_max; +} MdbSQLColumn; + +typedef struct { + char *name; + char *alias; +} MdbSQLTable; + +typedef struct { + char *col_name; + MdbSarg *sarg; +} MdbSQLSarg; + +#define mdb_sql_has_error(sql) ((sql)->error_msg[0] ? 1 : 0) +#define mdb_sql_last_error(sql) ((sql)->error_msg) + +void mdb_sql_error(MdbSQL* sql, const char *fmt, ...); +MdbSQL *mdb_sql_init(void); +MdbSQLSarg *mdb_sql_alloc_sarg(void); +MdbHandle *mdb_sql_open(MdbSQL *sql, char *db_name); +void mdb_sql_free_tree(MdbSargNode *tree); +int mdb_sql_add_sarg(MdbSQL *sql, char *col_name, int op, char *constant); +void mdb_sql_all_columns(MdbSQL *sql); +void mdb_sql_sel_count(MdbSQL *sql); +int mdb_sql_add_column(MdbSQL *sql, char *column_name); +int mdb_sql_add_table(MdbSQL *sql, char *table_name); +char *mdb_sql_strptime(MdbSQL *sql, char *data, char *format); +void mdb_sql_dump(MdbSQL *sql); +void mdb_sql_exit(MdbSQL *sql); +void mdb_sql_reset(MdbSQL *sql); +void mdb_sql_listtables(MdbSQL *sql); +void mdb_sql_select(MdbSQL *sql); +void mdb_sql_dump_node(MdbSargNode *node, int level); +void mdb_sql_close(MdbSQL *sql); +void mdb_sql_add_or(MdbSQL *sql); +void mdb_sql_add_and(MdbSQL *sql); +void mdb_sql_add_not(MdbSQL *sql); +void mdb_sql_describe_table(MdbSQL *sql); +MdbSQL* mdb_sql_run_query (MdbSQL*, const gchar*); +void mdb_sql_set_maxrow(MdbSQL *sql, int maxrow); +int mdb_sql_eval_expr(MdbSQL *sql, char *const1, int op, char *const2); +int mdb_sql_bind_all(MdbSQL *sql); +void mdb_sql_unbind_all(MdbSQL *sql); +int mdb_sql_fetch_row(MdbSQL *sql, MdbTableDef *table); +int mdb_sql_add_temp_col(MdbSQL *sql, MdbTableDef *ttable, int col_num, char *name, int col_type, int col_size, int is_fixed); +int mdb_sql_bind_column(MdbSQL *sql, int colnum, void *varaddr, int *len_ptr); +int mdb_sql_add_limit(MdbSQL *sql, char *limit, int percent); +int mdb_sql_get_limit(MdbSQL *sql); + +int parse_sql(MdbSQL * mdb, const gchar* str); + +#ifdef __cplusplus + } +#endif + +#endif diff --git a/wlx/dbview/include/mdbtools/mdbtools.h b/wlx/dbview/include/mdbtools/mdbtools.h new file mode 100644 index 0000000..fc83423 --- /dev/null +++ b/wlx/dbview/include/mdbtools/mdbtools.h @@ -0,0 +1,682 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#ifndef _mdbtools_h_ +#define _mdbtools_h_ + +#define MDBTOOLS_H_HAVE_ICONV_H 1 +#define MDBTOOLS_H_HAVE_XLOCALE_H 0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if MDBTOOLS_H_HAVE_ICONV_H +#include +#endif + +#if MDBTOOLS_H_HAVE_XLOCALE_H +#include +#endif + +#ifdef _WIN32 +#include +#endif + +#ifdef __cplusplus + extern "C" { +#endif + +/** \addtogroup mdbtools + * @{ + */ + +#define MDB_DEBUG 0 + +#define MDB_PGSIZE 4096 +//#define MDB_MAX_OBJ_NAME (256*3) /* unicode 16 -> utf-8 worst case */ +#define MDB_MAX_OBJ_NAME 256 +#define MDB_MAX_COLS 256 +#define MDB_MAX_IDX_COLS 10 +#define MDB_CATALOG_PG 18 +#define MDB_MEMO_OVERHEAD 12 +#define MDB_BIND_SIZE 16384 // override with mdb_set_bind_size(MdbHandle*, size_t) + +// This attribute is not supported by all compilers: +// M$VC see http://stackoverflow.com/questions/1113409/attribute-constructor-equivalent-in-vc +#define MDB_DEPRECATED(type, funcname) type __attribute__((deprecated)) funcname + +#ifdef __MINGW32__ + #include + #ifndef locale_t + typedef _locale_t locale_t; + #endif +#endif + +typedef locale_t mdb_locale_t; + +enum { + MDB_PAGE_DB = 0, + MDB_PAGE_DATA, + MDB_PAGE_TABLE, + MDB_PAGE_INDEX, + MDB_PAGE_LEAF, + MDB_PAGE_MAP +}; +enum { + MDB_VER_JET3 = 0, + MDB_VER_JET4 = 0x01, + MDB_VER_ACCDB_2007 = 0x02, + MDB_VER_ACCDB_2010 = 0x03, + MDB_VER_ACCDB_2013 = 0x04, + MDB_VER_ACCDB_2016 = 0x05, + MDB_VER_ACCDB_2019 = 0x06 +}; +enum { + MDB_FORM = 0, + MDB_TABLE, + MDB_MACRO, + MDB_SYSTEM_TABLE, + MDB_REPORT, + MDB_QUERY, + MDB_LINKED_TABLE, + MDB_MODULE, + MDB_RELATIONSHIP, + MDB_UNKNOWN_09, + MDB_UNKNOWN_0A, /* User access */ + MDB_DATABASE_PROPERTY, + MDB_ANY = -1 +}; +enum { + MDB_BOOL = 0x01, + MDB_BYTE = 0x02, + MDB_INT = 0x03, + MDB_LONGINT = 0x04, + MDB_MONEY = 0x05, + MDB_FLOAT = 0x06, + MDB_DOUBLE = 0x07, + MDB_DATETIME = 0x08, + MDB_BINARY = 0x09, + MDB_TEXT = 0x0a, + MDB_OLE = 0x0b, + MDB_MEMO = 0x0c, + MDB_REPID = 0x0f, + MDB_NUMERIC = 0x10, + MDB_COMPLEX = 0x12 +}; + +/* SARG operators */ +enum { + MDB_OR = 1, + MDB_AND, + MDB_NOT, + MDB_EQUAL, + MDB_GT, + MDB_LT, + MDB_GTEQ, + MDB_LTEQ, + MDB_LIKE, + MDB_ISNULL, + MDB_NOTNULL, + MDB_ILIKE, + MDB_NEQ, +}; + +typedef enum { + MDB_TABLE_SCAN, + MDB_LEAF_SCAN, + MDB_INDEX_SCAN +} MdbStrategy; + +typedef enum { + MDB_NOFLAGS = 0x00, + MDB_WRITABLE = 0x01 +} MdbFileFlags; + +enum { + MDB_DEBUG_LIKE = 0x0001, + MDB_DEBUG_WRITE = 0x0002, + MDB_DEBUG_USAGE = 0x0004, + MDB_DEBUG_OLE = 0x0008, + MDB_DEBUG_ROW = 0x0010, + MDB_DEBUG_PROPS = 0x0020, + MDB_USE_INDEX = 0x0040, + MDB_NO_MEMO = 0x0080, /* don't follow memo fields */ +}; + +typedef enum { + MDB_BRACES_4_2_2_8, /* "{XXXX-XX-XX-XXXXXXXX}" format */ + MDB_NOBRACES_4_2_2_2_6, /* "XXXX-XX-XX-XX-XXXXXX" format (matches MS Access ODBC driver) */ +} MdbUuidFormat; + +#define mdb_is_logical_op(x) (x == MDB_OR || \ + x == MDB_AND || \ + x == MDB_NOT ) + +#define mdb_is_relational_op(x) (x == MDB_EQUAL || \ + x == MDB_GT || \ + x == MDB_LT || \ + x == MDB_GTEQ || \ + x == MDB_LTEQ || \ + x == MDB_NEQ || \ + x == MDB_LIKE || \ + x == MDB_ILIKE || \ + x == MDB_ISNULL || \ + x == MDB_NOTNULL ) + +enum { + MDB_ASC, + MDB_DESC +}; + +enum { + MDB_IDX_UNIQUE = 0x01, + MDB_IDX_IGNORENULLS = 0x02, + MDB_IDX_REQUIRED = 0x08 +}; + +/* export schema options */ +enum { + MDB_SHEXP_DROPTABLE = 1<<0, /* issue drop table during export */ + MDB_SHEXP_CST_NOTNULL = 1<<1, /* generate NOT NULL constraints */ + MDB_SHEXP_CST_NOTEMPTY = 1<<2, /* <>'' constraints */ + MDB_SHEXP_COMMENTS = 1<<3, /* export comments on columns & tables */ + MDB_SHEXP_DEFVALUES = 1<<4, /* export default values */ + MDB_SHEXP_INDEXES = 1<<5, /* export indices */ + MDB_SHEXP_RELATIONS = 1<<6, /* export relation (foreign keys) */ + MDB_SHEXP_BULK_INSERT = 1 << 7 /* export data in bulk inserts */ +}; +#define MDB_SHEXP_DEFAULT (MDB_SHEXP_CST_NOTNULL | MDB_SHEXP_COMMENTS | MDB_SHEXP_INDEXES | MDB_SHEXP_RELATIONS) + +/* csv export binary options */ +enum { + MDB_BINEXPORT_STRIP, + MDB_BINEXPORT_RAW, + MDB_BINEXPORT_OCTAL, + MDB_BINEXPORT_HEXADECIMAL, + + /* Flags that can be OR'ed into the above when calling mdb_print_col */ + MDB_EXPORT_ESCAPE_CONTROL_CHARS = (1 << 4) +}; + +#define IS_JET4(mdb) (mdb->f->jet_version==MDB_VER_JET4) /* obsolete */ +#define IS_JET3(mdb) (mdb->f->jet_version==MDB_VER_JET3) + +/* forward declarations */ +typedef struct mdbindex MdbIndex; +typedef struct mdbsargtree MdbSargNode; + +typedef struct { + char *name; + unsigned char needs_precision; + unsigned char needs_scale; + unsigned char needs_byte_length; + unsigned char needs_char_length; +} MdbBackendType; + +typedef struct { + guint32 capabilities; /* see MDB_SHEXP_* */ + const MdbBackendType *types_table; + const MdbBackendType *type_shortdate; + const MdbBackendType *type_autonum; + const char *short_now; + const char *long_now; + const char *date_fmt; + const char *shortdate_fmt; + const char *charset_statement; + const char *drop_statement; + const char *constaint_not_empty_statement; + const char *column_comment_statement; + const char *per_column_comment_statement; + const char *table_comment_statement; + const char *per_table_comment_statement; + gchar* (*quote_schema_name)(const gchar*, const gchar*); + const char *create_table_statement; + gchar* (*normalise_case)(const gchar*); +} MdbBackend; + +typedef struct { + gboolean collect; + unsigned long pg_reads; +} MdbStatistics; + +typedef struct { + FILE *stream; + gboolean writable; + guint32 jet_version; + guint32 db_key; + char db_passwd[14]; + MdbStatistics *stats; + /* free map */ + int map_sz; + unsigned char *free_map; + /* reference count */ + int refs; + guint16 code_page; + guint16 lang_id; +} MdbFile; + +/* offset to row count on data pages...version dependant */ +typedef struct { + ssize_t pg_size; + guint16 row_count_offset; + guint16 tab_num_rows_offset; + guint16 tab_num_cols_offset; + guint16 tab_num_idxs_offset; + guint16 tab_num_ridxs_offset; + guint16 tab_usage_map_offset; + guint16 tab_first_dpg_offset; + guint16 tab_cols_start_offset; + guint16 tab_ridx_entry_size; + guint16 col_flags_offset; + guint16 col_size_offset; + guint16 col_num_offset; + guint16 tab_col_entry_size; + guint16 tab_free_map_offset; + guint16 tab_col_offset_var; + guint16 tab_col_offset_fixed; + guint16 tab_row_col_num_offset; + guint16 col_scale_offset; + guint16 col_prec_offset; +} MdbFormatConstants; + +typedef struct { + MdbFile *f; + guint32 cur_pg; + guint16 row_num; + unsigned int cur_pos; + unsigned char pg_buf[MDB_PGSIZE]; + unsigned char alt_pg_buf[MDB_PGSIZE]; + MdbFormatConstants *fmt; + size_t bind_size; + char date_fmt[64]; + char shortdate_fmt[64]; + MdbUuidFormat repid_fmt; + const char *boolean_false_value; + const char *boolean_true_value; + unsigned int num_catalog; + + // Non-cloneable fields start here + GPtrArray *catalog; + MdbBackend *default_backend; + char *backend_name; + struct S_MdbTableDef *relationships_table; + char *relationships_values[5]; + MdbStatistics *stats; + GHashTable *backends; +#if MDBTOOLS_H_HAVE_ICONV_H + iconv_t iconv_in; + iconv_t iconv_out; +#else + mdb_locale_t locale; +#endif +} MdbHandle; + +typedef struct { + MdbHandle *mdb; + char object_name[MDB_MAX_OBJ_NAME+1]; + int object_type; + unsigned long table_pg; /* misnomer since object may not be a table */ + //int num_props; please use props->len + GPtrArray *props; /* GPtrArray of MdbProperties */ + int flags; +} MdbCatalogEntry; + +typedef struct { + gchar *name; + GHashTable *hash; +} MdbProperties; + +typedef union { + int i; + double d; + char s[256]; +} MdbAny; + +struct S_MdbTableDef; /* forward definition */ +typedef struct { + struct S_MdbTableDef *table; + char name[MDB_MAX_OBJ_NAME+1]; + int col_type; + int col_size; + void *bind_ptr; + int *len_ptr; + GHashTable *properties; + unsigned int num_sargs; + GPtrArray *sargs; + GPtrArray *idx_sarg_cache; + unsigned char is_fixed; + int query_order; + /* col_num is the current column order, + * does not include deletes */ + int col_num; + int cur_value_start; + int cur_value_len; + /* MEMO/OLE readers */ + guint32 cur_blob_pg_row; + int chunk_size; + /* numerics only */ + int col_prec; + int col_scale; + unsigned char is_long_auto; + unsigned char is_uuid_auto; + MdbProperties *props; + /* info needed for handling deleted/added columns */ + int fixed_offset; + unsigned int var_col_num; + /* row_col_num is the row column number order, + * including deleted columns */ + int row_col_num; +} MdbColumn; + +struct mdbsargtree { + int op; + MdbColumn *col; + unsigned char val_type; + MdbAny value; + void *parent; + MdbSargNode *left; + MdbSargNode *right; +}; + +typedef struct { + guint32 pg; + int start_pos; + int offset; + int len; + int rc; + guint16 idx_starts[2000]; + unsigned char cache_value[256]; +} MdbIndexPage; + +typedef int (*MdbSargTreeFunc)(MdbSargNode *, gpointer data); + +#define MDB_MAX_INDEX_DEPTH 10 + +typedef struct { + int cur_depth; + guint32 last_leaf_found; + int clean_up_mode; + MdbIndexPage pages[MDB_MAX_INDEX_DEPTH]; +} MdbIndexChain; + +typedef struct S_MdbTableDef { + MdbCatalogEntry *entry; + char name[MDB_MAX_OBJ_NAME+1]; + unsigned int num_cols; + GPtrArray *columns; + unsigned int num_rows; + int index_start; + unsigned int num_real_idxs; + unsigned int num_idxs; + GPtrArray *indices; + guint32 first_data_pg; + guint32 cur_pg_num; + guint32 cur_phys_pg; + unsigned int cur_row; + int noskip_del; /* don't skip deleted rows */ + /* object allocation map */ + guint32 map_base_pg; + size_t map_sz; + unsigned char *usage_map; + /* pages with free space left */ + guint32 freemap_base_pg; + size_t freemap_sz; + unsigned char *free_usage_map; + /* query planner */ + MdbSargNode *sarg_tree; + MdbStrategy strategy; + MdbIndex *scan_idx; + MdbHandle *mdbidx; + MdbIndexChain *chain; + MdbProperties *props; + unsigned int num_var_cols; /* to know if row has variable columns */ + /* temp table */ + unsigned int is_temp_table; + GPtrArray *temp_table_pages; +} MdbTableDef; + +struct mdbindex { + int index_num; + char name[MDB_MAX_OBJ_NAME+1]; + unsigned char index_type; + guint32 first_pg; + int num_rows; /* number rows in index */ + unsigned int num_keys; + short key_col_num[MDB_MAX_IDX_COLS]; + unsigned char key_col_order[MDB_MAX_IDX_COLS]; + unsigned char flags; + MdbTableDef *table; +}; + +typedef struct { + char name[MDB_MAX_OBJ_NAME+1]; +} MdbColumnProp; + +typedef struct { + void *value; + int siz; + int start; + unsigned char is_null; + unsigned char is_fixed; + int colnum; + int offset; +} MdbField; + +typedef struct { + int op; + MdbAny value; +} MdbSarg; + +/* version.c */ +const char *mdb_get_version(void); + +/* file.c */ +ssize_t mdb_read_pg(MdbHandle *mdb, unsigned long pg); +ssize_t mdb_read_alt_pg(MdbHandle *mdb, unsigned long pg); +unsigned char mdb_get_byte(void *buf, int offset); +int mdb_get_int16(void *buf, int offset); +long mdb_get_int32(void *buf, int offset); +long mdb_get_int32_msb(void *buf, int offset); +float mdb_get_single(void *buf, int offset); +double mdb_get_double(void *buf, int offset); +unsigned char mdb_pg_get_byte(MdbHandle *mdb, int offset); +int mdb_pg_get_int16(MdbHandle *mdb, int offset); +long mdb_pg_get_int32(MdbHandle *mdb, int offset); +float mdb_pg_get_single(MdbHandle *mdb, int offset); +double mdb_pg_get_double(MdbHandle *mdb, int offset); +MdbHandle *mdb_open(const char *filename, MdbFileFlags flags); +MdbHandle *mdb_open_buffer(void *buffer, size_t len, MdbFileFlags flags); +void mdb_close(MdbHandle *mdb); +MdbHandle *mdb_clone_handle(MdbHandle *mdb); +void mdb_swap_pgbuf(MdbHandle *mdb); + +/* catalog.c */ +void mdb_free_catalog(MdbHandle *mdb); +GPtrArray *mdb_read_catalog(MdbHandle *mdb, int obj_type); +MdbCatalogEntry *mdb_get_catalogentry_by_name(MdbHandle *mdb, const gchar* name); +void mdb_dump_catalog(MdbHandle *mdb, int obj_type); +const char *mdb_get_objtype_string(int obj_type); + +/* table.c */ +MdbTableDef *mdb_alloc_tabledef(MdbCatalogEntry *entry); +void mdb_free_tabledef(MdbTableDef *table); +MdbTableDef *mdb_read_table(MdbCatalogEntry *entry); +MdbTableDef *mdb_read_table_by_name(MdbHandle *mdb, gchar *table_name, int obj_type); +void mdb_append_column(GPtrArray *columns, MdbColumn *in_col); +void mdb_free_columns(GPtrArray *columns); +GPtrArray *mdb_read_columns(MdbTableDef *table); +void mdb_table_dump(MdbCatalogEntry *entry); +guint8 read_pg_if_8(MdbHandle *mdb, int *cur_pos); +guint16 read_pg_if_16(MdbHandle *mdb, int *cur_pos); +guint32 read_pg_if_32(MdbHandle *mdb, int *cur_pos); +void *read_pg_if_n(MdbHandle *mdb, void *buf, int *cur_pos, size_t len); +int mdb_is_user_table(MdbCatalogEntry *entry); +int mdb_is_system_table(MdbCatalogEntry *entry); +const char *mdb_table_get_prop(const MdbTableDef *table, const gchar *key); +const char *mdb_col_get_prop(const MdbColumn *col, const gchar *key); +int mdb_col_is_shortdate(const MdbColumn *col); + +/* data.c */ +int mdb_bind_column_by_name(MdbTableDef *table, gchar *col_name, void *bind_ptr, int *len_ptr); +void mdb_data_dump(MdbTableDef *table); +void mdb_date_to_tm(double td, struct tm *t); +void mdb_tm_to_date(struct tm *t, double *td); +char *mdb_uuid_to_string(const void *buf, int start); /* Uses default MDB_BRACES_4_2_2_8 format */ +char *mdb_uuid_to_string_fmt(const void *buf, int start, MdbUuidFormat format); +int mdb_bind_column(MdbTableDef *table, int col_num, void *bind_ptr, int *len_ptr); +int mdb_rewind_table(MdbTableDef *table); +int mdb_fetch_row(MdbTableDef *table); +int mdb_is_fixed_col(MdbColumn *col); +char *mdb_col_to_string(MdbHandle *mdb, void *buf, int start, int datatype, int size); +int mdb_find_pg_row(MdbHandle *mdb, int pg_row, void **buf, int *off, size_t *len); +int mdb_find_row(MdbHandle *mdb, int row, int *start, size_t *len); +int mdb_find_end_of_row(MdbHandle *mdb, int row); +int mdb_col_fixed_size(MdbColumn *col); +int mdb_col_disp_size(MdbColumn *col); +size_t mdb_ole_read_next(MdbHandle *mdb, MdbColumn *col, void *ole_ptr); +size_t mdb_ole_read(MdbHandle *mdb, MdbColumn *col, void *ole_ptr, size_t chunk_size); +void* mdb_ole_read_full(MdbHandle *mdb, MdbColumn *col, size_t *size); +void mdb_set_bind_size(MdbHandle *mdb, size_t bind_size); +void mdb_set_date_fmt(MdbHandle *mdb, const char *); +void mdb_set_shortdate_fmt(MdbHandle *mdb, const char *); +void mdb_set_repid_fmt(MdbHandle *mdb, MdbUuidFormat format); +void mdb_set_boolean_fmt_words(MdbHandle *mdb); +void mdb_set_boolean_fmt_numbers(MdbHandle *mdb); +int mdb_read_row(MdbTableDef *table, unsigned int row); +int mdb_read_next_dpg(MdbTableDef *table); + +/* money.c */ +char *mdb_money_to_string(MdbHandle *mdb, int start); +char *mdb_numeric_to_string(MdbHandle *mdb, int start, int scale, int prec); + +/* dump.c */ +void mdb_buffer_dump(const void *buf, off_t start, size_t len); + +/* backend.c */ +void mdb_init_backends(MdbHandle *mdb); +void mdb_remove_backends(MdbHandle *mdb); +const MdbBackendType* mdb_get_colbacktype(const MdbColumn *col); +const char* mdb_get_colbacktype_string(const MdbColumn *col); +int mdb_colbacktype_takes_length(const MdbColumn *col); +void mdb_register_backend(MdbHandle *mdb, char *backend_name, guint32 capabilities, + const MdbBackendType *backend_type, + const MdbBackendType *type_shortdate, + const MdbBackendType *type_autonum, + const char *short_now, const char *long_now, + const char *date_fmt, const char *shortdate_fmt, + const char *charset_statement, const char *drop_statement, const char *constaint_not_empty_statement, + const char *column_comment_statement, const char *per_column_comment_statement, + const char *table_comment_statement, const char *per_table_comment_statement, + gchar* (*quote_schema_name)(const gchar*, const gchar*)); +int mdb_set_default_backend(MdbHandle *mdb, const char *backend_name); +int mdb_print_schema(MdbHandle *mdb, FILE *outfile, char *tabname, char *dbnamespace, guint32 export_options); +void mdb_print_col(FILE *outfile, gchar *col_val, int quote_text, int col_type, int bin_len, char *quote_char, char *escape_char, int flags); +gchar *mdb_normalise_and_replace(MdbHandle *mdb, gchar **str); + +/* sargs.c */ +int mdb_test_sargs(MdbTableDef *table, MdbField *fields, int num_fields); +int mdb_test_sarg(MdbHandle *mdb, MdbColumn *col, MdbSargNode *node, MdbField *field); +void mdb_sql_walk_tree(MdbSargNode *node, MdbSargTreeFunc func, gpointer data); +int mdb_find_indexable_sargs(MdbSargNode *node, gpointer data); +int mdb_add_sarg_by_name(MdbTableDef *table, char *colname, MdbSarg *in_sarg); +int mdb_test_string(MdbSargNode *node, char *s); +int mdb_test_int(MdbSargNode *node, gint32 i); +int mdb_add_sarg(MdbColumn *col, MdbSarg *in_sarg); + + + +/* index.c */ +GPtrArray *mdb_read_indices(MdbTableDef *table); +void mdb_index_dump(MdbTableDef *table, MdbIndex *idx); +void mdb_index_scan_free(MdbTableDef *table); +int mdb_index_find_next_on_page(MdbHandle *mdb, MdbIndexPage *ipg); +int mdb_index_find_next(MdbHandle *mdb, MdbIndex *idx, MdbIndexChain *chain, guint32 *pg, guint16 *row); +void mdb_index_hash_text(MdbHandle *mdb, char *text, char *hash); +void mdb_index_scan_init(MdbHandle *mdb, MdbTableDef *table); +int mdb_index_find_row(MdbHandle *mdb, MdbIndex *idx, MdbIndexChain *chain, guint32 pg, guint16 row); +void mdb_index_swap_n(unsigned char *src, int sz, unsigned char *dest); +void mdb_free_indices(GPtrArray *indices); +void mdb_index_page_reset(MdbHandle *mdb, MdbIndexPage *ipg); +int mdb_index_pack_bitmap(MdbHandle *mdb, MdbIndexPage *ipg); + +/* stats.c */ +void mdb_stats_on(MdbHandle *mdb); +void mdb_stats_off(MdbHandle *mdb); +void mdb_dump_stats(MdbHandle *mdb); + +/* like.c */ +int mdb_like_cmp(char *s, char *r); +int mdb_ilike_cmp(char *s, char *r); + +/* write.c */ +void mdb_put_int16(void *buf, guint32 offset, guint32 value); +void mdb_put_int32(void *buf, guint32 offset, guint32 value); +void mdb_put_int32_msb(void *buf, guint32 offset, guint32 value); +int mdb_crack_row(MdbTableDef *table, int row_start, size_t row_size, MdbField *fields); +guint16 mdb_add_row_to_pg(MdbTableDef *table, unsigned char *row_buffer, int new_row_size); +int mdb_update_index(MdbTableDef *table, MdbIndex *idx, unsigned int num_fields, MdbField *fields, guint32 pgnum, guint16 rownum); +int mdb_insert_row(MdbTableDef *table, int num_fields, MdbField *fields); +int mdb_pack_row(MdbTableDef *table, unsigned char *row_buffer, unsigned int num_fields, MdbField *fields); +int mdb_replace_row(MdbTableDef *table, int row, void *new_row, int new_row_size); +int mdb_pg_get_freespace(MdbHandle *mdb); +int mdb_update_row(MdbTableDef *table); +void *mdb_new_data_pg(MdbCatalogEntry *entry); + +/* map.c */ +gint32 mdb_map_find_next_freepage(MdbTableDef *table, int row_size); +gint32 mdb_map_find_next(MdbHandle *mdb, unsigned char *map, unsigned int map_sz, guint32 start_pg); + +/* props.c */ +void mdb_free_props(MdbProperties *props); +void mdb_dump_props(MdbProperties *props, FILE *outfile, int show_name); +GPtrArray* mdb_kkd_to_props(MdbHandle *mdb, void *kkd, size_t len); + + +/* worktable.c */ +MdbTableDef *mdb_create_temp_table(MdbHandle *mdb, char *name); +void mdb_temp_table_add_col(MdbTableDef *table, MdbColumn *col); +void mdb_fill_temp_col(MdbColumn *tcol, char *col_name, int col_size, int col_type, int is_fixed); +void mdb_fill_temp_field(MdbField *field, void *value, int siz, int is_fixed, int is_null, int start, int column); +void mdb_temp_columns_end(MdbTableDef *table); + +/* options.c */ +int mdb_get_option(unsigned long optnum); +void mdb_debug(int klass, char *fmt, ...); + +/* iconv.c */ +int mdb_unicode2ascii(MdbHandle *mdb, const char *src, size_t slen, char *dest, size_t dlen); +int mdb_ascii2unicode(MdbHandle *mdb, const char *src, size_t slen, char *dest, size_t dlen); +void mdb_iconv_init(MdbHandle *mdb); +void mdb_iconv_close(MdbHandle *mdb); +const char* mdb_target_charset(MdbHandle *mdb); + +/** @}*/ + +#ifdef __cplusplus + } +#endif + +#endif /* _mdbtools_h_ */ diff --git a/wlx/dbview/include/mdbtools/mdbver.h b/wlx/dbview/include/mdbtools/mdbver.h new file mode 100644 index 0000000..e564910 --- /dev/null +++ b/wlx/dbview/include/mdbtools/mdbver.h @@ -0,0 +1,25 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _mdbver_h_ +#define _mdbver_h_ + +#define MDB_FULL_VERSION "mdbtools v1.0.1" +#define MDB_VERSION_NO "1.0.1" + +#endif diff --git a/wlx/dbview/src/BdbEngine.cpp b/wlx/dbview/src/BdbEngine.cpp new file mode 100644 index 0000000..8844884 --- /dev/null +++ b/wlx/dbview/src/BdbEngine.cpp @@ -0,0 +1,162 @@ +#include "BdbEngine.h" +#include "KeyValueModel.h" + +#include +#include + +BdbEngine::BdbEngine(QObject *parent) + : DbEngine(parent) +{ +} + +BdbEngine::~BdbEngine() +{ + close(); +} + +bool BdbEngine::open(const QString &filepath) +{ + close(); + + int ret = db_create(&m_db, nullptr, 0); + if (ret != 0) { + return false; + } + + // Try read-write open (0 flag) + ret = m_db->open(m_db, nullptr, filepath.toLocal8Bit().constData(), nullptr, DB_UNKNOWN, 0, 0664); + if (ret != 0) { + // Fallback to read-only + ret = m_db->open(m_db, nullptr, filepath.toLocal8Bit().constData(), nullptr, DB_UNKNOWN, DB_RDONLY, 0); + if (ret != 0) { + m_db->close(m_db, 0); + m_db = nullptr; + return false; + } + m_readOnly = true; + } else { + m_readOnly = false; + } + + m_keyCount = countKeys(); + return true; +} + +void BdbEngine::close() +{ + delete m_model; + m_model = nullptr; + + if (m_db) { + m_db->close(m_db, 0); + m_db = nullptr; + } + m_keyCount = 0; +} + +int BdbEngine::countKeys() const +{ + if (!m_db) return 0; + + DBC *dbcp = nullptr; + int ret = m_db->cursor(m_db, nullptr, &dbcp, 0); + if (ret != 0) return 0; + + int count = 0; + DBT key, data; + memset(&key, 0, sizeof(DBT)); + memset(&data, 0, sizeof(DBT)); + + while (dbcp->get(dbcp, &key, &data, DB_NEXT) == 0) { + count++; + } + + dbcp->close(dbcp); + return count; +} + +QStringList BdbEngine::tableNames() const +{ + return { QStringLiteral("") }; +} + +QString BdbEngine::currentTableName() const +{ + return QStringLiteral(""); +} + +QAbstractItemModel *BdbEngine::modelForTable(const QString &tableName) +{ + Q_UNUSED(tableName); + if (!m_db) return nullptr; + + delete m_model; + m_model = nullptr; + + DB *db = m_db; + + KeyValueModel::IteratorOps ops; + + ops.fetchWindow = [db](int startIndex, int count) -> QVector { + QVector entries; + entries.reserve(count); + + DBC *dbcp = nullptr; + int ret = db->cursor(db, nullptr, &dbcp, 0); + if (ret != 0) return entries; + + DBT key, data; + memset(&key, 0, sizeof(DBT)); + memset(&data, 0, sizeof(DBT)); + + // Skip to startIndex + ret = dbcp->get(dbcp, &key, &data, DB_FIRST); + if (ret == 0) { + for (int i = 0; i < startIndex; ++i) { + ret = dbcp->get(dbcp, &key, &data, DB_NEXT); + if (ret != 0) break; + } + // Collect entries + if (ret == 0) { + for (int i = 0; i < count; ++i) { + KeyValueModel::Entry e; + e.key = QByteArray(static_cast(key.data), static_cast(key.size)); + e.value = QByteArray(static_cast(data.data), static_cast(data.size)); + entries.append(std::move(e)); + ret = dbcp->get(dbcp, &key, &data, DB_NEXT); + if (ret != 0) break; + } + } + } + + dbcp->close(dbcp); + return entries; + }; + + ops.putValue = [db](const QByteArray &key, const QByteArray &value) -> bool { + DBT k, v; + memset(&k, 0, sizeof(DBT)); + k.data = (void*)key.constData(); + k.size = key.size(); + + memset(&v, 0, sizeof(DBT)); + v.data = (void*)value.constData(); + v.size = value.size(); + + int ret = db->put(db, nullptr, &k, &v, 0); + return ret == 0; + }; + + ops.deleteKey = [db](const QByteArray &key) -> bool { + DBT k; + memset(&k, 0, sizeof(DBT)); + k.data = (void*)key.constData(); + k.size = key.size(); + + int ret = db->del(db, nullptr, &k, 0); + return ret == 0; + }; + + m_model = new KeyValueModel(m_keyCount, std::move(ops), this); + return m_model; +} diff --git a/wlx/dbview/src/DbEngine.cpp b/wlx/dbview/src/DbEngine.cpp new file mode 100644 index 0000000..fb7d275 --- /dev/null +++ b/wlx/dbview/src/DbEngine.cpp @@ -0,0 +1,112 @@ +#include "DbEngine.h" + +#include "SqliteEngine.h" +#include "DuckDbEngine.h" +#include "FirebirdEngine.h" +#include "MdbEngine.h" +#include "BdbEngine.h" +#include "LmdbEngine.h" + +#ifdef ENABLE_ROCKSDB_LEVELDB +#include "LevelDbEngine.h" +#include "RocksDbEngine.h" +#endif + +#include +#include + +std::unique_ptr DbEngine::createForFile(const QString &filepath) +{ + QFileInfo info(filepath); + QString ext = info.suffix().toLower(); + + // --- SQL engines --- + + // SQLite + if (ext == QStringLiteral("sqlite") || ext == QStringLiteral("sqlite3") + || ext == QStringLiteral("db") || ext == QStringLiteral("db3")) { + auto engine = std::make_unique(); + if (engine->open(filepath)) + return engine; + // Fallback: might be a DuckDB file or Berkeley DB with .db extension + } + + // DuckDB & Parquet (via DuckDB proxy) + if (ext == QStringLiteral("duckdb") || ext == QStringLiteral("parquet") || ext == QStringLiteral("pq")) { + auto engine = std::make_unique(); + if (engine->open(filepath)) + return engine; + } + + // Firebird Embedded + if (ext == QStringLiteral("fdb")) { + auto engine = std::make_unique(); + if (engine->open(filepath)) + return engine; + } + + // --- KV engines --- + + // LMDB: data.mdb or file ending in .lmdb + if (ext == QStringLiteral("lmdb") || info.fileName() == QStringLiteral("data.mdb")) { + auto engine = std::make_unique(); + if (engine->open(filepath)) + return engine; + } + + // Berkeley DB: .bdb or fallback for .db + if (ext == QStringLiteral("bdb")) { + auto engine = std::make_unique(); + if (engine->open(filepath)) + return engine; + } + +#ifdef ENABLE_ROCKSDB_LEVELDB + // LevelDB: files inside a LevelDB directory + if (ext == QStringLiteral("ldb") || ext == QStringLiteral("sst") + || ext == QStringLiteral("log")) { + // Check if parent dir looks like a LevelDB directory + QString parentDir = info.absolutePath(); + if (QFile::exists(parentDir + QStringLiteral("/CURRENT"))) { + auto engine = std::make_unique(); + if (engine->open(filepath)) + return engine; + } + } + + // RocksDB: same file extensions as LevelDB, try if LevelDB failed + if (ext == QStringLiteral("sst") || ext == QStringLiteral("log")) { + QString parentDir = info.absolutePath(); + if (QFile::exists(parentDir + QStringLiteral("/CURRENT"))) { + auto engine = std::make_unique(); + if (engine->open(filepath)) + return engine; + } + } +#endif + + // MS Access: .mdb, .accdb (after LMDB check since data.mdb is LMDB) + if (ext == QStringLiteral("mdb") || ext == QStringLiteral("accdb")) { + auto engine = std::make_unique(); + if (engine->open(filepath)) + return engine; + } + + // --- Fallback: try SQLite/Berkeley DB for any unknown/shared extension --- + { + auto engine = std::make_unique(); + if (engine->open(filepath)) { + // Check if it actually has tables + if (!engine->tableNames().isEmpty() || !engine->viewNames().isEmpty()) + return engine; + } + } + { + auto engine = std::make_unique(); + if (engine->open(filepath)) { + return engine; + } + } + + return nullptr; +} diff --git a/wlx/dbview/src/DbViewWidget.cpp b/wlx/dbview/src/DbViewWidget.cpp new file mode 100644 index 0000000..e958d11 --- /dev/null +++ b/wlx/dbview/src/DbViewWidget.cpp @@ -0,0 +1,849 @@ +#include "DbViewWidget.h" +#include "DbEngine.h" +#include "KeyValueModel.h" +#include "DuckDbModel.h" +#include "MdbEngine.h" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace QtWlPlugin; + +namespace { +class DbSortFilterProxyModel : public QSortFilterProxyModel { +public: + using QSortFilterProxyModel::QSortFilterProxyModel; + + void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override { + if (auto *duckModel = qobject_cast(sourceModel())) { + duckModel->sort(column, order); + } else { + QSortFilterProxyModel::sort(column, order); + } + } +}; +} + +// --------------------------------------------------------------------------- +// Construction / destruction +// --------------------------------------------------------------------------- + +DbViewWidget::DbViewWidget(QWidget *parent) + : QWidget(parent) +{ +} + +DbViewWidget::~DbViewWidget() +{ + if (m_fm) { + m_fm->setActive(false); + delete m_fm; + } + if (m_schemaTree) + disconnect(m_schemaTree, nullptr, this, nullptr); + if (m_filterProxy) + m_filterProxy->setSourceModel(nullptr); + + if (m_sqlResultsView) { + QAbstractItemModel *m = m_sqlResultsView->model(); + m_sqlResultsView->setModel(nullptr); + if (m) { + delete m; + } + } +} + +// --------------------------------------------------------------------------- +// File loading +// --------------------------------------------------------------------------- + +bool DbViewWidget::loadFile(const QString &filepath) +{ + m_engine = DbEngine::createForFile(filepath); + if (!m_engine) + return false; + + QStringList tables = m_engine->tableNames(); + QStringList views = m_engine->viewNames(); + + if (tables.isEmpty() && views.isEmpty()) + return false; + + QString firstTable = tables.isEmpty() ? views.first() : tables.first(); + setupUi(firstTable); + + ThemeManager::applyTheme(this, ThemeManager::detectSystemTheme()); + return true; +} + +// --------------------------------------------------------------------------- +// UI setup +// --------------------------------------------------------------------------- + +void DbViewWidget::setupUi(const QString &firstTable) +{ + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + // Primary view + m_tableView = new QTableView; + m_tableView->setObjectName(QStringLiteral("mainTableView")); + m_filterHeader = new FilterableHeaderView(Qt::Horizontal, m_tableView); + m_filterHeader->setFilterEnabled(true); + m_filterHeader->setStretchLastSection(true); + m_tableView->setHorizontalHeader(m_filterHeader); + + m_tableView->setAlternatingRowColors(true); + m_tableView->setSortingEnabled(true); + m_tableView->setSelectionBehavior(QAbstractItemView::SelectItems); + m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection); + + m_fm = new FocusManager(this, m_tableView, this); + + setupToolbar(); + mainLayout->addWidget(m_toolbar); + + // Read-only constraints on grid + if (m_engine->isReadOnly()) { + m_tableView->setEditTriggers(QAbstractItemView::NoEditTriggers); + } else { + m_tableView->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed | QAbstractItemView::AnyKeyPressed); + } + + // Splitter for top and bottom + m_mainSplitter = new QSplitter(Qt::Vertical, this); + + // Data Browser (top panel) + QWidget *dataBrowserWidget = new QWidget; + QHBoxLayout *dataBrowserLayout = new QHBoxLayout(dataBrowserWidget); + dataBrowserLayout->setContentsMargins(0, 0, 0, 0); + dataBrowserLayout->setSpacing(0); + + QSplitter *browserSplitter = new QSplitter(Qt::Horizontal, dataBrowserWidget); + + // Schema navigation tree (left) + m_schemaTree = new QTreeView; + m_schemaTree->setHeaderHidden(true); + m_schemaTree->setMinimumWidth(150); + m_schemaTree->setMaximumWidth(350); + populateSchemaTree(); + + connect(m_schemaTree, &QTreeView::clicked, this, [this](const QModelIndex &index) { + QStandardItemModel *model = qobject_cast(m_schemaTree->model()); + if (!model) return; + QStandardItem *item = model->itemFromIndex(index); + if (!item) return; + + QVariant tVar = item->data(Qt::UserRole); + if (tVar.isValid()) { + QString tableName = tVar.toString(); + if (!tableName.isEmpty() && tableName != m_engine->currentTableName()) { + rebuildGrid(tableName); + if (m_filterHeader) + m_filterHeader->clearFilters(); + } + } + }); + + browserSplitter->addWidget(m_schemaTree); + + // Grid Container (right) + QWidget *rightContainer = new QWidget; + QVBoxLayout *rightLayout = new QVBoxLayout(rightContainer); + rightLayout->setContentsMargins(0, 0, 0, 0); + rightLayout->setSpacing(0); + + rebuildGrid(firstTable); + + m_grid = new EditableGridWidget(m_tableView, GridMode::LiveDatabase, m_fm, this); + + connect(m_filterHeader, &FilterableHeaderView::filterChanged, this, + [this](int column, const QString &text) { + if (m_filterProxy) { + m_filterProxy->setFilterKeyColumn(column); + m_filterProxy->setFilterFixedString(text); + updateStatusBar(); + } + }); + + setupContextMenu(); + rightLayout->addWidget(m_grid, 1); + browserSplitter->addWidget(rightContainer); + + dataBrowserLayout->addWidget(browserSplitter); + m_mainSplitter->addWidget(dataBrowserWidget); + + // SQL Console (bottom panel) + m_sqlConsoleWidget = new QWidget; + QVBoxLayout *sqlLayout = new QVBoxLayout(m_sqlConsoleWidget); + sqlLayout->setContentsMargins(0, 4, 0, 0); + sqlLayout->setSpacing(4); + + m_sqlEditor = new QPlainTextEdit; + m_sqlEditor->setPlaceholderText(QStringLiteral("Enter custom SQL query (Ctrl+Return to execute)...")); + m_sqlEditor->setMaximumHeight(100); + m_sqlEditor->installEventFilter(this); + m_fm->addInputWidget(m_sqlEditor); + + // SQL Toolbar + QWidget *sqlToolbarWidget = new QWidget; + QHBoxLayout *sqlToolbarLayout = new QHBoxLayout(sqlToolbarWidget); + sqlToolbarLayout->setContentsMargins(4, 0, 4, 0); + sqlToolbarLayout->setSpacing(6); + + QPushButton *btnExecute = new QPushButton(style()->standardIcon(QStyle::SP_MediaPlay), QStringLiteral("Execute")); + QPushButton *btnClear = new QPushButton(style()->standardIcon(QStyle::SP_DialogDiscardButton), QStringLiteral("Clear")); + QPushButton *btnExport = new QPushButton(style()->standardIcon(QStyle::SP_DialogSaveButton), QStringLiteral("Export")); + + connect(btnExecute, &QPushButton::clicked, this, &DbViewWidget::onExecuteSqlQuery); + connect(btnClear, &QPushButton::clicked, this, &DbViewWidget::onClearSqlConsole); + connect(btnExport, &QPushButton::clicked, this, &DbViewWidget::onExportSqlResults); + + sqlToolbarLayout->addWidget(btnExecute); + sqlToolbarLayout->addWidget(btnClear); + sqlToolbarLayout->addWidget(btnExport); + sqlToolbarLayout->addStretch(); + + // Results Stack: Page 0 = table view, Page 1 = message view + m_sqlResultsStack = new QStackedWidget(m_sqlConsoleWidget); + + m_sqlResultsView = new QTableView(m_sqlResultsStack); + m_sqlResultsView->setAlternatingRowColors(true); + m_sqlResultsView->setSelectionBehavior(QAbstractItemView::SelectItems); + m_sqlResultsView->setSelectionMode(QAbstractItemView::ExtendedSelection); + + m_sqlMessageView = new QTextEdit(m_sqlResultsStack); + m_sqlMessageView->setReadOnly(true); + m_fm->addInputWidget(m_sqlResultsView); + m_fm->addInputWidget(m_sqlMessageView); + + m_sqlResultsStack->addWidget(m_sqlResultsView); + m_sqlResultsStack->addWidget(m_sqlMessageView); + + sqlLayout->addWidget(m_sqlEditor); + sqlLayout->addWidget(sqlToolbarWidget); + sqlLayout->addWidget(m_sqlResultsStack, 1); + + m_mainSplitter->addWidget(m_sqlConsoleWidget); + mainLayout->addWidget(m_mainSplitter, 1); + + // Show/Hide Bottom SQL Console depending on engine + // We no longer hide m_schemaTree for single-table engines so tables list is visible. + if (!m_engine->supportsSqlConsole()) { + m_sqlConsoleWidget->hide(); + } + + // Find/replace + setupFindReplace(); + mainLayout->addWidget(m_findPanel); + m_findPanel->setVisible(false); + + // Status bar + m_statusBar = new PluginStatusBar(this); + m_statusBar->setFormatInfo(m_engine->engineName()); + mainLayout->addWidget(m_statusBar); + + updateStatusBar(); +} + +void DbViewWidget::populateSchemaTree() +{ + auto *model = new QStandardItemModel(m_schemaTree); + + QStandardItem *tablesRoot = new QStandardItem(style()->standardIcon(QStyle::SP_DirIcon), QStringLiteral("Tables")); + QStandardItem *viewsRoot = new QStandardItem(style()->standardIcon(QStyle::SP_DirIcon), QStringLiteral("Views")); + + QStringList tables = m_engine->tableNames(); + QStringList views = m_engine->viewNames(); + + for (const auto &t : tables) { + QStandardItem *tableItem = new QStandardItem(style()->standardIcon(QStyle::SP_FileIcon), t); + tableItem->setData(t, Qt::UserRole); + + // Columns + QStandardItem *colsRoot = new QStandardItem(QStringLiteral("Columns")); + QList cols = m_engine->columnInfos(t); + for (const auto &c : cols) { + QString label = c.name + QStringLiteral(" : ") + c.type; + if (c.isPrimaryKey) label += QStringLiteral(" [PK]"); + if (c.isForeignKey) label += QStringLiteral(" [FK]"); + QStandardItem *colItem = new QStandardItem(style()->standardIcon(QStyle::SP_FileDialogInfoView), label); + colsRoot->appendRow(colItem); + } + tableItem->appendRow(colsRoot); + + // Indexes + QStringList idxs = m_engine->indexes(t); + if (!idxs.isEmpty()) { + QStandardItem *idxsRoot = new QStandardItem(QStringLiteral("Indexes")); + for (const auto &idx : idxs) { + QStandardItem *idxItem = new QStandardItem(style()->standardIcon(QStyle::SP_ComputerIcon), idx); + idxsRoot->appendRow(idxItem); + } + tableItem->appendRow(idxsRoot); + } + + tablesRoot->appendRow(tableItem); + } + + for (const auto &v : views) { + QStandardItem *viewItem = new QStandardItem(style()->standardIcon(QStyle::SP_FileIcon), v); + viewItem->setData(v, Qt::UserRole); + + QStandardItem *colsRoot = new QStandardItem(QStringLiteral("Columns")); + QList cols = m_engine->columnInfos(v); + for (const auto &c : cols) { + QString label = c.name + QStringLiteral(" : ") + c.type; + QStandardItem *colItem = new QStandardItem(style()->standardIcon(QStyle::SP_FileDialogInfoView), label); + colsRoot->appendRow(colItem); + } + viewItem->appendRow(colsRoot); + viewsRoot->appendRow(viewItem); + } + + if (tablesRoot->rowCount() > 0) + model->appendRow(tablesRoot); + if (viewsRoot->rowCount() > 0) + model->appendRow(viewsRoot); + + m_schemaTree->setModel(model); + m_schemaTree->expandToDepth(0); +} + +void DbViewWidget::setupToolbar() +{ + m_toolbar = new PluginToolBar(m_fm, this); + + // Commit / Revert + if (m_engine->supportsSubmitRevert()) { + m_actSubmit = m_toolbar->addToolAction( + QStringLiteral("Commit"), + QKeySequence(Qt::CTRL | Qt::Key_S), + FocusManager::Always); + connect(m_actSubmit, &QAction::triggered, this, &DbViewWidget::onSubmitChanges); + + m_actRevert = m_toolbar->addToolAction( + QStringLiteral("Revert"), + QKeySequence(Qt::CTRL | Qt::Key_Z), + FocusManager::WhenNoInput); + connect(m_actRevert, &QAction::triggered, this, &DbViewWidget::onRevertChanges); + + // Register Ctrl+Shift+Z as secondary redo/revert shortcut + QShortcut *redoShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Z), this); + connect(redoShortcut, &QShortcut::activated, this, &DbViewWidget::onSubmitChanges); + + if (m_engine->isReadOnly()) { + m_actSubmit->setEnabled(false); + m_actRevert->setEnabled(false); + redoShortcut->setEnabled(false); + } + } + + // Word wrap + auto *actWrap = m_toolbar->addToolAction(QStringLiteral("Word Wrap"), QKeySequence(), 0); + actWrap->setCheckable(true); + connect(actWrap, &QAction::toggled, this, [this](bool on) { + if (m_grid) m_grid->setWordWrap(on); + }); + + // Grid lines + auto *actGrid = m_toolbar->addToolAction(QStringLiteral("Grid Lines"), QKeySequence(), 0); + actGrid->setCheckable(true); + actGrid->setChecked(true); + connect(actGrid, &QAction::toggled, this, [this](bool on) { + if (m_grid) m_grid->setShowGrid(on); + }); + + // Find + auto *actFind = m_toolbar->addToolAction( + QStringLiteral("Find"), + QKeySequence(Qt::CTRL | Qt::Key_F), + FocusManager::Always); + connect(actFind, &QAction::triggered, this, [this]() { + if (m_findPanel) + m_findPanel->showPanel(!m_findPanel->isPanelVisible()); + }); + + // Export table data + auto *actExport = m_toolbar->addToolAction( + QStringLiteral("Export"), + QKeySequence(), 0); + connect(actExport, &QAction::triggered, this, &DbViewWidget::onExportTableData); +} + +void DbViewWidget::setupFindReplace() +{ + m_findPanel = new FindReplacePanel(m_fm, this); + m_findPanel->setReplaceEnabled(false); + + connect(m_findPanel, &FindReplacePanel::findRequested, + this, &DbViewWidget::onFind); +} + +void DbViewWidget::setupContextMenu() +{ + m_grid->setExtraContextMenuCallback( + [this](QMenu *menu, const QModelIndex &idx) { + if (!idx.isValid()) return; + + QAbstractItemModel *model = m_tableView->model(); + auto *kvModel = qobject_cast(model); + QModelIndex srcIdx = idx; + if (auto *proxy = qobject_cast(model)) { + kvModel = qobject_cast(proxy->sourceModel()); + srcIdx = proxy->mapToSource(idx); + } + + // Hex toggle for KeyValueModel + if (kvModel && srcIdx.column() == 1 && kvModel->isBinaryValue(srcIdx.row())) { + int row = srcIdx.row(); + QAction *hexAction = menu->addAction(QStringLiteral("Toggle Hex View")); + connect(hexAction, &QAction::triggered, this, [kvModel, row]() { + bool currentlyHex = kvModel->data(kvModel->index(row, 1), Qt::DisplayRole) + .toString().contains(QStringLiteral(" ")); + kvModel->setHexMode(row, !currentlyHex); + }); + } + + // Save Cell to File (BLOB Export) + QAction *saveAction = menu->addAction(QStringLiteral("Save Cell to File (BLOB Export)...")); + connect(saveAction, &QAction::triggered, this, [this, idx]() { + QByteArray data = getCellRawValue(idx); + QString path = QFileDialog::getSaveFileName(nullptr, QStringLiteral("Save BLOB Value")); + if (path.isEmpty()) return; + QFile f(path); + if (f.open(QIODevice::WriteOnly)) { + f.write(data); + f.close(); + } + }); + + // Load File into Cell (BLOB Import) - only if NOT read-only + if (!m_engine->isReadOnly()) { + QAction *loadAction = menu->addAction(QStringLiteral("Load File into Cell (BLOB Import)...")); + connect(loadAction, &QAction::triggered, this, [this, idx]() { + QString path = QFileDialog::getOpenFileName(nullptr, QStringLiteral("Load BLOB Value")); + if (path.isEmpty()) return; + QFile f(path); + if (f.open(QIODevice::ReadOnly)) { + QByteArray fileData = f.readAll(); + f.close(); + setCellRawValue(idx, fileData); + } + }); + } + }); +} + +// --------------------------------------------------------------------------- +// BLOB helpers +// --------------------------------------------------------------------------- + +bool DbViewWidget::isCellBinary(const QModelIndex &idx) const +{ + if (!idx.isValid()) return false; + QAbstractItemModel *model = m_tableView->model(); + QModelIndex srcIdx = idx; + if (auto *proxy = qobject_cast(model)) { + model = proxy->sourceModel(); + srcIdx = proxy->mapToSource(idx); + } + + if (auto *kvModel = qobject_cast(model)) { + return srcIdx.column() == 1 && kvModel->isBinaryValue(srcIdx.row()); + } + if (auto *duckModel = qobject_cast(model)) { + return duckModel->isBinaryValue(srcIdx.row(), srcIdx.column()); + } + if (auto *mdbModel = qobject_cast(model)) { + return mdbModel->isBinaryValue(srcIdx.row(), srcIdx.column()); + } + if (auto *sqlModel = qobject_cast(model)) { + QSqlRecord rec = sqlModel->record(); + QSqlField f = rec.field(srcIdx.column()); + return f.metaType().id() == QMetaType::QByteArray; + } + return false; +} + +QByteArray DbViewWidget::getCellRawValue(const QModelIndex &idx) const +{ + if (!idx.isValid()) return {}; + QAbstractItemModel *model = m_tableView->model(); + QModelIndex srcIdx = idx; + if (auto *proxy = qobject_cast(model)) { + model = proxy->sourceModel(); + srcIdx = proxy->mapToSource(idx); + } + + if (auto *kvModel = qobject_cast(model)) { + return kvModel->rawValue(srcIdx.row()); + } + if (auto *duckModel = qobject_cast(model)) { + return duckModel->rawValue(srcIdx.row(), srcIdx.column()); + } + if (auto *mdbModel = qobject_cast(model)) { + return mdbModel->rawValue(srcIdx.row(), srcIdx.column()); + } + if (auto *sqlModel = qobject_cast(model)) { + return sqlModel->data(srcIdx, Qt::EditRole).toByteArray(); + } + return {}; +} + +bool DbViewWidget::setCellRawValue(const QModelIndex &idx, const QByteArray &bytes) +{ + if (!idx.isValid() || m_engine->isReadOnly()) return false; + QAbstractItemModel *model = m_tableView->model(); + QModelIndex srcIdx = idx; + if (auto *proxy = qobject_cast(model)) { + model = proxy->sourceModel(); + srcIdx = proxy->mapToSource(idx); + } + + if (auto *kvModel = qobject_cast(model)) { + return kvModel->loadValueFromFile(srcIdx.row(), bytes); + } + if (auto *duckModel = qobject_cast(model)) { + return duckModel->setData(srcIdx, bytes, Qt::EditRole); + } + if (auto *sqlModel = qobject_cast(model)) { + return sqlModel->setData(srcIdx, bytes, Qt::EditRole); + } + return false; +} + +// --------------------------------------------------------------------------- +// Table switching +// --------------------------------------------------------------------------- + +void DbViewWidget::rebuildGrid(const QString &tableName) +{ + m_tableView->setModel(nullptr); + if (m_filterProxy) + m_filterProxy->setSourceModel(nullptr); + + QAbstractItemModel *model = m_engine->modelForTable(tableName); + if (!model) return; + + if (!m_filterProxy) { + m_filterProxy = new DbSortFilterProxyModel(this); + m_filterProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + } + m_filterProxy->setSourceModel(model); + m_tableView->setModel(m_filterProxy); + + updateStatusBar(); +} + +void DbViewWidget::updateStatusBar() +{ + if (!m_statusBar) return; + + int total = 0; + int filtered = 0; + if (m_tableView && m_tableView->model()) { + filtered = m_tableView->model()->rowCount(); + if (m_filterProxy && m_filterProxy->sourceModel()) { + total = m_filterProxy->sourceModel()->rowCount(); + } else { + total = filtered; + } + } + + m_statusBar->setRowCount(filtered, total); + + if (m_engine) + m_statusBar->setEncoding(m_engine->currentTableName()); +} + +// --------------------------------------------------------------------------- +// Data operations +// --------------------------------------------------------------------------- + +void DbViewWidget::onSubmitChanges() +{ + if (!m_engine || !m_engine->supportsSubmitRevert() || m_engine->isReadOnly()) return; + + if (!m_engine->submitAll()) { + QMessageBox::warning(nullptr, QStringLiteral("Commit Error"), + QStringLiteral("Failed to commit changes:\n%1").arg(m_engine->lastError())); + m_engine->revertAll(); + } + updateStatusBar(); +} + +void DbViewWidget::onRevertChanges() +{ + if (!m_engine || !m_engine->supportsSubmitRevert() || m_engine->isReadOnly()) return; + + m_engine->revertAll(); + updateStatusBar(); +} + +// --------------------------------------------------------------------------- +// SQL Console Slots +// --------------------------------------------------------------------------- + +void DbViewWidget::onExecuteSqlQuery() +{ + QString query = m_sqlEditor->toPlainText().trimmed(); + if (query.isEmpty()) return; + + if (m_engine->isReadOnly()) { + if (!query.startsWith(QStringLiteral("SELECT"), Qt::CaseInsensitive) && + !query.startsWith(QStringLiteral("PRAGMA"), Qt::CaseInsensitive) && + !query.startsWith(QStringLiteral("SHOW"), Qt::CaseInsensitive) && + !query.startsWith(QStringLiteral("DESCRIBE"), Qt::CaseInsensitive)) { + m_sqlMessageView->setHtml(QStringLiteral("Read-Only Constraint:
" + "This database is opened in read-only mode. Only SELECT/read queries are allowed.")); + m_sqlResultsStack->setCurrentWidget(m_sqlMessageView); + return; + } + } + + QAbstractItemModel *model = m_engine->executeQuery(query); + if (!model) { + QAbstractItemModel *oldModel = m_sqlResultsView->model(); + m_sqlResultsView->setModel(nullptr); + if (oldModel) { + delete oldModel; + } + + if (m_engine->lastQueryError()) { + m_sqlMessageView->setHtml(QStringLiteral("SQL Query Error:
%1") + .arg(m_engine->lastError().toHtmlEscaped())); + } else { + m_sqlMessageView->setHtml(QStringLiteral("Query Executed Successfully:
%1") + .arg(m_engine->lastError().isEmpty() ? QStringLiteral("Query executed successfully.") : m_engine->lastError().toHtmlEscaped())); + } + m_sqlResultsStack->setCurrentWidget(m_sqlMessageView); + return; + } + + QAbstractItemModel *oldModel = m_sqlResultsView->model(); + m_sqlResultsView->setModel(model); + if (oldModel) { + delete oldModel; + } + m_sqlResultsStack->setCurrentWidget(m_sqlResultsView); +} + +void DbViewWidget::onClearSqlConsole() +{ + m_sqlEditor->clear(); + QAbstractItemModel *oldModel = m_sqlResultsView->model(); + m_sqlResultsView->setModel(nullptr); + if (oldModel) { + delete oldModel; + } + m_sqlMessageView->clear(); + m_sqlResultsStack->setCurrentWidget(m_sqlResultsView); +} + +void DbViewWidget::onExportSqlResults() +{ + QAbstractItemModel *model = m_sqlResultsView->model(); + if (!model || model->rowCount() == 0) { + QMessageBox::information(nullptr, QStringLiteral("Export Results"), QStringLiteral("No results to export.")); + return; + } + + QString filter = QStringLiteral("CSV Files (*.csv);;TSV Files (*.tsv)"); + QString selectedFilter; + QString path = QFileDialog::getSaveFileName(nullptr, QStringLiteral("Export Results"), QString(), filter, &selectedFilter); + if (path.isEmpty()) return; + + QChar sep = selectedFilter.contains(QStringLiteral("TSV")) ? QLatin1Char('\t') : QLatin1Char(','); + + QFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(nullptr, QStringLiteral("Export Error"), QStringLiteral("Failed to open file for writing.")); + return; + } + + QTextStream out(&f); + + // Headers + int cols = model->columnCount(); + for (int col = 0; col < cols; ++col) { + QString header = model->headerData(col, Qt::Horizontal).toString(); + header.replace(QStringLiteral("\""), QStringLiteral("\"\"")); + if (col > 0) out << sep; + out << QStringLiteral("\"%1\"").arg(header); + } + out << QStringLiteral("\n"); + + // Rows + int rows = model->rowCount(); + for (int row = 0; row < rows; ++row) { + for (int col = 0; col < cols; ++col) { + QString val = model->data(model->index(row, col), Qt::DisplayRole).toString(); + val.replace(QStringLiteral("\""), QStringLiteral("\"\"")); + if (col > 0) out << sep; + out << QStringLiteral("\"%1\"").arg(val); + } + out << QStringLiteral("\n"); + } + + f.close(); +} + +void DbViewWidget::onExportTableData() +{ + QAbstractItemModel *model = m_tableView ? m_tableView->model() : nullptr; + if (!model || model->rowCount() == 0) { + QMessageBox::information(nullptr, QStringLiteral("Export Table"), + QStringLiteral("No data to export.")); + return; + } + + QString tableName = m_engine ? m_engine->currentTableName() : QStringLiteral("table"); + QString filter = QStringLiteral("CSV Files (*.csv);;TSV Files (*.tsv)"); + QString selectedFilter; + QString path = QFileDialog::getSaveFileName( + nullptr, QStringLiteral("Export Table"), tableName, filter, &selectedFilter); + if (path.isEmpty()) return; + + QChar sep = selectedFilter.contains(QStringLiteral("TSV")) ? QLatin1Char('\t') : QLatin1Char(','); + + QFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(nullptr, QStringLiteral("Export Error"), + QStringLiteral("Failed to open file for writing.")); + return; + } + + QTextStream out(&f); + + // Headers + int cols = model->columnCount(); + for (int col = 0; col < cols; ++col) { + QString header = model->headerData(col, Qt::Horizontal).toString(); + header.replace(QStringLiteral("\""), QStringLiteral("\"\"")); + if (col > 0) out << sep; + out << QStringLiteral("\"%1\"").arg(header); + } + out << QStringLiteral("\n"); + + // Rows + int rows = model->rowCount(); + for (int row = 0; row < rows; ++row) { + for (int col = 0; col < cols; ++col) { + QString val = model->data(model->index(row, col), Qt::DisplayRole).toString(); + val.replace(QStringLiteral("\""), QStringLiteral("\"\"")); + if (col > 0) out << sep; + out << QStringLiteral("\"%1\"").arg(val); + } + out << QStringLiteral("\n"); + } + + f.close(); +} + +// --------------------------------------------------------------------------- +// Find +// --------------------------------------------------------------------------- + +void DbViewWidget::onFind(bool forward) +{ + if (!m_findPanel || !m_tableView || !m_tableView->model()) + return; + + QString query = m_findPanel->findText(); + if (query.isEmpty()) return; + + bool caseSensitive = m_findPanel->matchCase(); + QAbstractItemModel *model = m_tableView->model(); + QModelIndex current = m_tableView->currentIndex(); + + int startRow = current.isValid() ? current.row() : 0; + int startCol = current.isValid() ? current.column() : 0; + int rows = model->rowCount(); + int cols = model->columnCount(); + int total = rows * cols; + + if (forward) { + startCol++; + if (startCol >= cols) { startCol = 0; startRow++; } + } else { + startCol--; + if (startCol < 0) { startCol = cols - 1; startRow--; } + } + + for (int i = 0; i < total; ++i) { + int offset = forward ? i : -i; + int linearStart = startRow * cols + startCol; + int linearIdx = (linearStart + offset + total) % total; + int r = linearIdx / cols; + int c = linearIdx % cols; + + QModelIndex idx = model->index(r, c); + QString cellText = model->data(idx, Qt::DisplayRole).toString(); + + bool match = caseSensitive + ? cellText.contains(query, Qt::CaseSensitive) + : cellText.contains(query, Qt::CaseInsensitive); + + if (match) { + m_tableView->setCurrentIndex(idx); + m_tableView->scrollTo(idx); + m_findPanel->setStatusText(QStringLiteral("Found at row %1, col %2").arg(r + 1).arg(c + 1)); + return; + } + } + + m_findPanel->setStatusText(QStringLiteral("\"%1\" not found").arg(query)); +} + +// --------------------------------------------------------------------------- +// WLX bridge accessors +// --------------------------------------------------------------------------- + +FocusManager *DbViewWidget::focusManager() const { return m_fm; } +EditableGridWidget *DbViewWidget::grid() const { return m_grid; } + +QString DbViewWidget::getSelectionAsText(char sep) +{ + return m_grid ? m_grid->getSelectionAsText(sep) : QString(); +} + +bool DbViewWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == m_sqlEditor && event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) && + (keyEvent->modifiers() & Qt::ControlModifier)) { + onExecuteSqlQuery(); + return true; // Consume event + } + } + return QWidget::eventFilter(obj, event); +} diff --git a/wlx/dbview/src/DuckDbEngine.cpp b/wlx/dbview/src/DuckDbEngine.cpp new file mode 100644 index 0000000..568cc2e --- /dev/null +++ b/wlx/dbview/src/DuckDbEngine.cpp @@ -0,0 +1,310 @@ +#include "DuckDbEngine.h" +#include "DuckDbModel.h" + +#include "duckdb.hpp" + +#include +#include + +DuckDbEngine::DuckDbEngine(QObject *parent) + : DbEngine(parent) +{ +} + +DuckDbEngine::~DuckDbEngine() +{ + close(); +} + +bool DuckDbEngine::open(const QString &filepath) +{ + close(); + + try { + duckdb::DBConfig config; + config.SetOptionByName("threads", duckdb::Value::INTEGER(1)); + + bool isParquet = filepath.endsWith(QStringLiteral(".parquet"), Qt::CaseInsensitive) || + filepath.endsWith(QStringLiteral(".pq"), Qt::CaseInsensitive); + + if (isParquet) { + m_duckdb = std::make_unique("", &config); + m_conn = std::make_unique(*m_duckdb); + + QFileInfo fi(filepath); + QString baseName = fi.baseName(); + QString cleanName = ""; + for (QChar c : baseName) { + if (c.isLetterOrNumber() || c == '_') { + cleanName.append(c); + } + } + if (cleanName.isEmpty()) { + cleanName = QStringLiteral("parquet_data"); + } + + QString escapedPath = filepath; + escapedPath.replace(QStringLiteral("'"), QStringLiteral("''")); + + QString query = QStringLiteral("CREATE VIEW \"%1\" AS SELECT * FROM read_parquet('%2')") + .arg(cleanName) + .arg(escapedPath); + + auto res = m_conn->Query(query.toStdString()); + if (!res || res->HasError()) { + m_lastError = QString::fromStdString(res ? res->GetError() : "Unknown error creating parquet view"); + return false; + } + m_readOnly = true; + m_inTransaction = false; + } else { + // Try read-write first + try { + m_duckdb = std::make_unique(filepath.toStdString(), &config); + m_conn = std::make_unique(*m_duckdb); + m_conn->Query("BEGIN TRANSACTION"); + m_inTransaction = true; + m_readOnly = false; + } catch (...) { + // Fallback to read-only + config.SetOptionByName("access_mode", duckdb::Value("READ_ONLY")); + m_duckdb = std::make_unique(filepath.toStdString(), &config); + m_conn = std::make_unique(*m_duckdb); + m_inTransaction = false; + m_readOnly = true; + } + } + + return true; + } catch (const std::exception &e) { + m_lastError = QString::fromStdString(e.what()); + return false; + } +} + +void DuckDbEngine::close() +{ + delete m_currentModel; + m_currentModel = nullptr; + + if (m_inTransaction && m_conn) { + try { m_conn->Query("ROLLBACK"); } catch (...) {} + m_inTransaction = false; + } + + m_conn.reset(); + m_duckdb.reset(); + m_currentTable.clear(); +} + +QStringList DuckDbEngine::tableNames() const +{ + QStringList result; + if (!m_conn) return result; + + try { + auto qr = m_conn->Query( + "SELECT table_name FROM information_schema.tables " + "WHERE table_schema = 'main' AND table_type = 'BASE TABLE' " + "ORDER BY table_name"); + if (qr && !qr->HasError()) { + for (auto &row : *qr) { + result.append(QString::fromStdString(row.GetValue(0))); + } + } + } catch (...) {} + return result; +} + +QStringList DuckDbEngine::viewNames() const +{ + QStringList result; + if (!m_conn) return result; + + try { + auto qr = m_conn->Query( + "SELECT table_name FROM information_schema.tables " + "WHERE table_schema = 'main' AND table_type = 'VIEW' " + "ORDER BY table_name"); + if (qr && !qr->HasError()) { + for (auto &row : *qr) { + result.append(QString::fromStdString(row.GetValue(0))); + } + } + } catch (...) {} + return result; +} + +QList DuckDbEngine::columnInfos(const QString &tableName) const +{ + QList result; + if (!m_conn) return result; + + try { + std::string q = "SELECT column_name, data_type FROM information_schema.columns " + "WHERE table_name = '" + tableName.toStdString() + "' " + "AND table_schema = 'main' ORDER BY ordinal_position"; + auto qr = m_conn->Query(q); + if (qr && !qr->HasError()) { + for (auto &row : *qr) { + ColumnInfo info; + info.name = QString::fromStdString(row.GetValue(0)); + info.type = QString::fromStdString(row.GetValue(1)); + result.append(info); + } + } + + std::string pi = "PRAGMA table_info('" + tableName.toStdString() + "')"; + auto pir = m_conn->Query(pi); + if (pir && !pir->HasError()) { + for (auto &row : *pir) { + QString colName = QString::fromStdString(row.GetValue(1)); + int pk = row.GetValue(5); + if (pk > 0) { + for (auto &info : result) { + if (info.name == colName) { + info.isPrimaryKey = true; + break; + } + } + } + } + } + } catch (...) {} + + return result; +} + +QStringList DuckDbEngine::indexes(const QString &tableName) const +{ + QStringList result; + if (!m_conn) return result; + + try { + std::string q = "SELECT index_name FROM duckdb_indexes WHERE table_name = '" + tableName.toStdString() + "'"; + auto qr = m_conn->Query(q); + if (qr && !qr->HasError()) { + for (auto &row : *qr) { + result.append(QString::fromStdString(row.GetValue(0))); + } + } + } catch (...) {} + return result; +} + +QAbstractItemModel *DuckDbEngine::modelForTable(const QString &tableName) +{ + if (!m_conn) return nullptr; + + delete m_currentModel; + m_currentModel = nullptr; + m_currentTable = tableName; + + auto *model = new DuckDbModel(m_conn.get(), tableName, this); + if (!model->select()) { + delete model; + return nullptr; + } + + m_currentModel = model; + return model; +} + +QString DuckDbEngine::currentTableName() const +{ + return m_currentTable; +} + +bool DuckDbEngine::submitAll() +{ + if (!m_conn || !m_inTransaction || m_readOnly) return false; + + try { + auto qr = m_conn->Query("COMMIT"); + if (qr && qr->HasError()) { + m_lastError = QString::fromStdString(qr->GetError()); + return false; + } + m_conn->Query("BEGIN TRANSACTION"); + m_inTransaction = true; + return true; + } catch (const std::exception &e) { + m_lastError = QString::fromStdString(e.what()); + return false; + } +} + +bool DuckDbEngine::revertAll() +{ + if (!m_conn || !m_inTransaction || m_readOnly) return false; + + try { + m_conn->Query("ROLLBACK"); + m_conn->Query("BEGIN TRANSACTION"); + m_inTransaction = true; + + if (!m_currentTable.isEmpty()) { + auto *model = qobject_cast(m_currentModel); + if (model) model->select(); + } + return true; + } catch (const std::exception &e) { + m_lastError = QString::fromStdString(e.what()); + return false; + } +} + +QAbstractItemModel *DuckDbEngine::executeQuery(const QString &query) +{ + if (!m_conn) return nullptr; + + m_lastQueryError = false; + m_lastError.clear(); + + try { + auto result = m_conn->Query(query.toStdString()); + if (!result) { + m_lastQueryError = true; + m_lastError = QStringLiteral("Unknown error executing query."); + return nullptr; + } + if (result->HasError()) { + m_lastQueryError = true; + m_lastError = QString::fromStdString(result->GetError()); + return nullptr; + } + + int colCount = result->ColumnCount(); + if (colCount == 0) { + m_lastQueryError = false; + m_lastError = QStringLiteral("Query executed successfully."); + return nullptr; + } + + if (colCount == 1 && result->ColumnName(0) == "Count") { + int64_t count = 0; + for (auto &row : *result) { + count = row.GetValue(0); + } + m_lastQueryError = false; + m_lastError = QStringLiteral("Query executed successfully, %1 row(s) affected.").arg(count); + return nullptr; + } + + auto *model = new DuckDbModel(m_conn.get(), query, this); + if (!model->select()) { + delete model; + return nullptr; + } + return model; + } catch (const std::exception &e) { + m_lastQueryError = true; + m_lastError = QString::fromStdString(e.what()); + return nullptr; + } +} + +QString DuckDbEngine::lastError() const +{ + return m_lastError; +} diff --git a/wlx/dbview/src/DuckDbModel.cpp b/wlx/dbview/src/DuckDbModel.cpp new file mode 100644 index 0000000..0bd76fe --- /dev/null +++ b/wlx/dbview/src/DuckDbModel.cpp @@ -0,0 +1,300 @@ +#include "DuckDbModel.h" + +#include "duckdb.hpp" + +#include +#include + +DuckDbModel::DuckDbModel(duckdb::Connection *conn, const QString &tableNameOrQuery, QObject *parent) + : QAbstractTableModel(parent) + , m_conn(conn) +{ + QString trimmed = tableNameOrQuery.trimmed(); + if (trimmed.contains(QLatin1Char(' ')) || trimmed.startsWith(QStringLiteral("SELECT"), Qt::CaseInsensitive)) { + m_query = trimmed; + m_isQuery = true; + } else { + m_tableName = trimmed; + m_isQuery = false; + } +} + +bool DuckDbModel::select() +{ + beginResetModel(); + m_data.clear(); + m_rowIds.clear(); + m_columnNames.clear(); + m_totalRows = 0; + m_allFetched = false; + + try { + if (m_isQuery) { + m_allFetched = true; + m_hasRowId = false; + + auto result = m_conn->Query(m_query.toStdString()); + if (!result || result->HasError()) { + endResetModel(); + return false; + } + + // Get column names + int colCount = result->ColumnCount(); + for (int i = 0; i < colCount; ++i) { + m_columnNames.append(QString::fromStdString(result->ColumnName(i))); + } + + // Fetch all rows + for (auto &row : *result) { + QVector rowData; + rowData.reserve(colCount); + for (int c = 0; c < colCount; ++c) { + try { + auto val = row.GetValue(c); + if (val.IsNull()) { + rowData.append(QVariant()); + } else if (val.type().id() == duckdb::LogicalTypeId::BLOB) { + std::string blobData = val.GetValue(); + rowData.append(QByteArray(blobData.data(), blobData.size())); + } else { + rowData.append(QString::fromStdString(val.ToString())); + } + } catch (...) { + rowData.append(QVariant()); + } + } + m_data.append(std::move(rowData)); + } + m_totalRows = m_data.size(); + } else { + // Check if rowid is queryable + m_hasRowId = false; + try { + auto testResult = m_conn->Query("SELECT rowid FROM \"" + m_tableName.toStdString() + "\" LIMIT 0"); + if (testResult && !testResult->HasError()) { + m_hasRowId = true; + } + } catch (...) { + m_hasRowId = false; + } + + // Get column names + auto colResult = m_conn->Query( + "SELECT column_name FROM information_schema.columns " + "WHERE table_name = '" + m_tableName.toStdString() + "' " + "AND table_schema = 'main' ORDER BY ordinal_position"); + if (colResult && !colResult->HasError()) { + for (auto &row : *colResult) { + m_columnNames.append(QString::fromStdString(row.GetValue(0))); + } + } + + // Get total row count + auto countResult = m_conn->Query( + "SELECT COUNT(*) FROM \"" + m_tableName.toStdString() + "\""); + if (countResult && !countResult->HasError()) { + for (auto &row : *countResult) { + m_totalRows = static_cast(row.GetValue(0)); + } + } + + // Load initial chunk + loadChunk(0, kChunkSize); + } + } catch (const std::exception &e) { + qWarning() << "DuckDbModel::select failed:" << e.what(); + } + + endResetModel(); + return !m_columnNames.isEmpty(); +} + +void DuckDbModel::loadChunk(int offset, int limit) +{ + if (m_isQuery) return; + + try { + std::string orderBy; + if (m_sortColumn >= 0 && m_sortColumn < m_columnNames.size()) { + std::string colName = m_columnNames[m_sortColumn].toStdString(); + std::string orderStr = (m_sortOrder == Qt::AscendingOrder) ? "ASC" : "DESC"; + orderBy = "ORDER BY \"" + colName + "\" " + orderStr + " "; + } + + std::string selectFields = m_hasRowId ? "rowid, *" : "*"; + std::string sql = "SELECT " + selectFields + " FROM \"" + m_tableName.toStdString() + "\" " + + orderBy + + "LIMIT " + std::to_string(limit) + " " + + "OFFSET " + std::to_string(offset); + + auto result = m_conn->Query(sql); + if (!result || result->HasError()) + return; + + int fetchedCount = 0; + for (auto &row : *result) { + if (m_hasRowId) { + m_rowIds.append(row.GetValue(0)); + } + QVector rowData; + rowData.reserve(m_columnNames.size()); + int startCol = m_hasRowId ? 1 : 0; + for (int c = startCol; c < startCol + m_columnNames.size(); ++c) { + try { + auto val = row.GetValue(c); + if (val.IsNull()) { + rowData.append(QVariant()); + } else if (val.type().id() == duckdb::LogicalTypeId::BLOB) { + std::string blobData = val.GetValue(); + rowData.append(QByteArray(blobData.data(), blobData.size())); + } else { + rowData.append(QString::fromStdString(val.ToString())); + } + } catch (...) { + rowData.append(QVariant()); + } + } + m_data.append(std::move(rowData)); + ++fetchedCount; + } + + if (fetchedCount < limit) + m_allFetched = true; + + } catch (const std::exception &e) { + qWarning() << "DuckDbModel::loadChunk failed:" << e.what(); + } +} + +int DuckDbModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_data.size(); +} + +int DuckDbModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_columnNames.size(); +} + +QVariant DuckDbModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_data.size() || index.column() >= m_columnNames.size()) + return {}; + + if (role != Qt::DisplayRole && role != Qt::EditRole) + return {}; + + const QVariant &val = m_data[index.row()][index.column()]; + if (role == Qt::DisplayRole) { + if (val.userType() == QMetaType::QByteArray) { + return QStringLiteral("[Binary Data - %1 bytes]").arg(val.toByteArray().size()); + } + if (val.isNull()) + return QStringLiteral("NULL"); + } + return val; +} + +QVariant DuckDbModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return {}; + if (section < 0 || section >= m_columnNames.size()) + return {}; + return m_columnNames[section]; +} + +Qt::ItemFlags DuckDbModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + if (m_hasRowId && !m_isQuery) + f |= Qt::ItemIsEditable; + return f; +} + +bool DuckDbModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role != Qt::EditRole || !index.isValid() || !m_hasRowId || m_isQuery) + return false; + + if (index.row() >= m_data.size() || index.column() >= m_columnNames.size()) + return false; + + try { + std::string colName = m_columnNames[index.column()].toStdString(); + std::string newVal = value.toString().toStdString(); + int64_t rowId = m_rowIds[index.row()]; + + // Use rowid-based UPDATE + std::string sql = "UPDATE \"" + m_tableName.toStdString() + "\" SET \"" + + colName + "\" = '" + newVal + "' WHERE rowid = " + + std::to_string(rowId); + + auto result = m_conn->Query(sql); + if (result && !result->HasError()) { + m_data[index.row()][index.column()] = value; + emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); + return true; + } + } catch (...) {} + + return false; +} + +void DuckDbModel::sort(int column, Qt::SortOrder order) +{ + if (m_isQuery) return; + if (m_sortColumn == column && m_sortOrder == order) + return; + + m_sortColumn = column; + m_sortOrder = order; + + beginResetModel(); + m_data.clear(); + m_rowIds.clear(); + m_allFetched = false; + loadChunk(0, kChunkSize); + endResetModel(); +} + +void DuckDbModel::fetchMore(const QModelIndex &parent) +{ + if (m_isQuery || parent.isValid() || m_allFetched) + return; + + int currentSize = m_data.size(); + int toFetch = qMin(kChunkSize, m_totalRows - currentSize); + if (toFetch <= 0) { + m_allFetched = true; + return; + } + + beginInsertRows(QModelIndex(), currentSize, currentSize + toFetch - 1); + loadChunk(currentSize, toFetch); + endInsertRows(); +} + +bool DuckDbModel::canFetchMore(const QModelIndex &parent) const +{ + if (m_isQuery || parent.isValid()) + return false; + return !m_allFetched && m_data.size() < m_totalRows; +} + +bool DuckDbModel::isBinaryValue(int row, int col) const +{ + if (row < 0 || row >= m_data.size() || col < 0 || col >= m_columnNames.size()) + return false; + return m_data[row][col].userType() == QMetaType::QByteArray; +} + +QByteArray DuckDbModel::rawValue(int row, int col) const +{ + if (row < 0 || row >= m_data.size() || col < 0 || col >= m_columnNames.size()) + return {}; + return m_data[row][col].toByteArray(); +} diff --git a/wlx/dbview/src/FirebirdEngine.cpp b/wlx/dbview/src/FirebirdEngine.cpp new file mode 100644 index 0000000..2a7e958 --- /dev/null +++ b/wlx/dbview/src/FirebirdEngine.cpp @@ -0,0 +1,284 @@ +#include "FirebirdEngine.h" + +#include +#include +#include +#include +#include +#include + +namespace { +QString getFirebirdTypeName(int typeId) { + switch (typeId) { + case 7: return QStringLiteral("SMALLINT"); + case 8: return QStringLiteral("INTEGER"); + case 16: return QStringLiteral("BIGINT"); + case 10: return QStringLiteral("FLOAT"); + case 27: return QStringLiteral("DOUBLE"); + case 14: return QStringLiteral("CHAR"); + case 37: return QStringLiteral("VARCHAR"); + case 35: return QStringLiteral("TIMESTAMP"); + case 12: return QStringLiteral("DATE"); + case 13: return QStringLiteral("TIME"); + case 261: return QStringLiteral("BLOB"); + default: return QStringLiteral("UNKNOWN(%1)").arg(typeId); + } +} +} + +FirebirdEngine::FirebirdEngine(QObject *parent) + : DbEngine(parent) + , m_connectionName(QUuid::createUuid().toString(QUuid::WithoutBraces)) +{ +} + +FirebirdEngine::~FirebirdEngine() +{ + close(); +} + +bool FirebirdEngine::open(const QString &filepath) +{ + close(); + + m_db = QSqlDatabase::addDatabase(QStringLiteral("QIBASE"), m_connectionName); + m_db.setDatabaseName(filepath); + m_db.setUserName(QStringLiteral("SYSDBA")); + m_db.setPassword(QStringLiteral("masterkey")); + + // Set connection options + m_db.setConnectOptions(QStringLiteral("ISC_DPB_LC_CTYPE=UTF8")); + + if (!m_db.open()) { + m_readOnly = true; + // Try opening read-only (which might succeed if write lock/permissions fail but DB is readable) + m_db.setDatabaseName(filepath); + if (!m_db.open()) { + return false; + } + } else { + m_readOnly = false; + } + + return true; +} + +void FirebirdEngine::close() +{ + if (m_currentModel) { + delete m_currentModel; + m_currentModel = nullptr; + } + m_currentTable.clear(); + + if (m_db.isOpen()) + m_db.close(); + + if (QSqlDatabase::connectionNames().contains(m_connectionName)) + QSqlDatabase::removeDatabase(m_connectionName); +} + +QStringList FirebirdEngine::tableNames() const +{ + QStringList result; + if (!m_db.isOpen()) return result; + + QSqlQuery q(m_db); + q.prepare(QStringLiteral( + "SELECT TRIM(rdb$relation_name) FROM rdb$relations " + "WHERE rdb$view_blr IS NULL AND (rdb$system_flag IS NULL OR rdb$system_flag = 0) " + "ORDER BY rdb$relation_name" + )); + if (q.exec()) { + while (q.next()) { + result.append(q.value(0).toString()); + } + } + return result; +} + +QStringList FirebirdEngine::viewNames() const +{ + QStringList result; + if (!m_db.isOpen()) return result; + + QSqlQuery q(m_db); + q.prepare(QStringLiteral( + "SELECT TRIM(rdb$relation_name) FROM rdb$relations " + "WHERE rdb$view_blr IS NOT NULL AND (rdb$system_flag IS NULL OR rdb$system_flag = 0) " + "ORDER BY rdb$relation_name" + )); + if (q.exec()) { + while (q.next()) { + result.append(q.value(0).toString()); + } + } + return result; +} + +QList FirebirdEngine::columnInfos(const QString &tableName) const +{ + QList result; + if (!m_db.isOpen()) return result; + + // First find primary keys + QStringList pks; + QSqlQuery pkQuery(m_db); + pkQuery.prepare(QStringLiteral( + "SELECT TRIM(isg.rdb$field_name) " + "FROM rdb$relation_constraints rc " + "JOIN rdb$index_segments isg ON rc.rdb$index_name = isg.rdb$index_name " + "WHERE rc.rdb$relation_name = ? AND rc.rdb$constraint_type = 'PRIMARY KEY'" + )); + pkQuery.addBindValue(tableName.trimmed().toUpper()); + if (pkQuery.exec()) { + while (pkQuery.next()) { + pks.append(pkQuery.value(0).toString()); + } + } + + // Find foreign keys + QStringList fks; + QSqlQuery fkQuery(m_db); + fkQuery.prepare(QStringLiteral( + "SELECT TRIM(isg.rdb$field_name) " + "FROM rdb$relation_constraints rc " + "JOIN rdb$index_segments isg ON rc.rdb$index_name = isg.rdb$index_name " + "WHERE rc.rdb$relation_name = ? AND rc.rdb$constraint_type = 'FOREIGN KEY'" + )); + fkQuery.addBindValue(tableName.trimmed().toUpper()); + if (fkQuery.exec()) { + while (fkQuery.next()) { + fks.append(fkQuery.value(0).toString()); + } + } + + // Query columns + QSqlQuery q(m_db); + q.prepare(QStringLiteral( + "SELECT TRIM(rf.rdb$field_name), f.rdb$field_type, f.rdb$field_length " + "FROM rdb$relation_fields rf " + "JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name " + "WHERE rf.rdb$relation_name = ? " + "ORDER BY rf.rdb$field_position" + )); + q.addBindValue(tableName.trimmed().toUpper()); + + if (q.exec()) { + while (q.next()) { + ColumnInfo info; + info.name = q.value(0).toString(); + int typeId = q.value(1).toInt(); + int len = q.value(2).toInt(); + info.type = getFirebirdTypeName(typeId); + if (typeId == 14 || typeId == 37) { // CHAR/VARCHAR + info.type += QStringLiteral("(%1)").arg(len); + } + info.isPrimaryKey = pks.contains(info.name); + info.isForeignKey = fks.contains(info.name); + result.append(info); + } + } + return result; +} + +QStringList FirebirdEngine::indexes(const QString &tableName) const +{ + QStringList result; + if (!m_db.isOpen()) return result; + + QSqlQuery q(m_db); + q.prepare(QStringLiteral( + "SELECT TRIM(rdb$index_name) FROM rdb$indices " + "WHERE rdb$relation_name = ? AND (rdb$system_flag IS NULL OR rdb$system_flag = 0)" + )); + q.addBindValue(tableName.trimmed().toUpper()); + + if (q.exec()) { + while (q.next()) { + result.append(q.value(0).toString()); + } + } + return result; +} + +QAbstractItemModel *FirebirdEngine::modelForTable(const QString &tableName) +{ + if (!m_db.isOpen()) return nullptr; + + if (m_currentModel) { + delete m_currentModel; + m_currentModel = nullptr; + } + + m_currentTable = tableName; + + auto *model = new QSqlTableModel(this, m_db); + model->setTable(tableName); + model->setEditStrategy(QSqlTableModel::OnManualSubmit); + model->select(); + + while (model->canFetchMore()) + model->fetchMore(); + + m_currentModel = model; + return model; +} + +QString FirebirdEngine::currentTableName() const +{ + return m_currentTable; +} + +bool FirebirdEngine::submitAll() +{ + if (!m_currentModel) return false; + return m_currentModel->submitAll(); +} + +bool FirebirdEngine::revertAll() +{ + if (!m_currentModel) return false; + m_currentModel->revertAll(); + return true; +} + +QAbstractItemModel *FirebirdEngine::executeQuery(const QString &query) +{ + if (!m_db.isOpen()) return nullptr; + + m_lastQueryError = false; + m_lastError.clear(); + + QSqlQuery q(m_db); + if (!q.exec(query)) { + m_lastQueryError = true; + m_lastError = q.lastError().text(); + return nullptr; + } + + if (q.isSelect()) { + m_lastQueryError = false; + auto *model = new QSqlQueryModel(this); + model->setQuery(std::move(q)); + return model; + } else { + m_lastQueryError = false; + int affected = q.numRowsAffected(); + if (affected < 0) { + m_lastError = QStringLiteral("Query executed successfully."); + } else { + m_lastError = QStringLiteral("Query executed successfully, %1 row(s) affected.").arg(affected); + } + return nullptr; + } +} + +QString FirebirdEngine::lastError() const +{ + if (m_lastQueryError || !m_lastError.isEmpty()) + return m_lastError; + if (m_currentModel) + return m_currentModel->lastError().text(); + return m_db.lastError().text(); +} diff --git a/wlx/dbview/src/KeyValueModel.cpp b/wlx/dbview/src/KeyValueModel.cpp new file mode 100644 index 0000000..53b173b --- /dev/null +++ b/wlx/dbview/src/KeyValueModel.cpp @@ -0,0 +1,177 @@ +#include "KeyValueModel.h" + +#include + +KeyValueModel::KeyValueModel(int totalRows, IteratorOps ops, QObject *parent) + : QAbstractTableModel(parent) + , m_totalRows(totalRows) + , m_ops(std::move(ops)) +{ +} + +int KeyValueModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_totalRows; +} + +int KeyValueModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : 2; // Key, Value +} + +QVariant KeyValueModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return {}; + return section == 0 ? QStringLiteral("Key") : QStringLiteral("Value"); +} + +Qt::ItemFlags KeyValueModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + // Keys are read-only, values are editable + if (index.column() == 0) + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; +} + +void KeyValueModel::ensureCached(int row) const +{ + if (m_cacheStartRow >= 0 && row >= m_cacheStartRow + && row < m_cacheStartRow + m_cache.size()) + return; + + // Center the window on the requested row + int start = qMax(0, row - kWindowSize / 2); + int count = qMin(kWindowSize, m_totalRows - start); + + m_cache = m_ops.fetchWindow(start, count); + m_cacheStartRow = start; +} + +QVariant KeyValueModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_totalRows) + return {}; + + if (role != Qt::DisplayRole && role != Qt::EditRole) + return {}; + + ensureCached(index.row()); + + int localIdx = index.row() - m_cacheStartRow; + if (localIdx < 0 || localIdx >= m_cache.size()) + return QStringLiteral("[Out of range]"); + + const Entry &entry = m_cache[localIdx]; + const QByteArray &raw = (index.column() == 0) ? entry.key : entry.value; + + // Binary value handling (column 1 only) + if (index.column() == 1 && !isValidUtf8(raw)) { + if (m_hexRows.contains(index.row())) + return toHexString(raw); + return formatBinaryPlaceholder(raw.size()); + } + + // Keys and valid UTF-8 values + if (isValidUtf8(raw)) + return QString::fromUtf8(raw); + + // Binary key (unusual but possible) + return toHexString(raw); +} + +bool KeyValueModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role != Qt::EditRole || index.column() != 1 || !index.isValid()) + return false; + + ensureCached(index.row()); + int localIdx = index.row() - m_cacheStartRow; + if (localIdx < 0 || localIdx >= m_cache.size()) + return false; + + QByteArray key = m_cache[localIdx].key; + QByteArray newValue = value.toString().toUtf8(); + + if (m_ops.putValue && m_ops.putValue(key, newValue)) { + m_cache[localIdx].value = newValue; + emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); + return true; + } + return false; +} + +bool KeyValueModel::isBinaryValue(int row) const +{ + ensureCached(row); + int localIdx = row - m_cacheStartRow; + if (localIdx < 0 || localIdx >= m_cache.size()) + return false; + return !isValidUtf8(m_cache[localIdx].value); +} + +QByteArray KeyValueModel::rawValue(int row) const +{ + ensureCached(row); + int localIdx = row - m_cacheStartRow; + if (localIdx < 0 || localIdx >= m_cache.size()) + return {}; + return m_cache[localIdx].value; +} + +QByteArray KeyValueModel::rawKey(int row) const +{ + ensureCached(row); + int localIdx = row - m_cacheStartRow; + if (localIdx < 0 || localIdx >= m_cache.size()) + return {}; + return m_cache[localIdx].key; +} + +void KeyValueModel::setHexMode(int row, bool enabled) +{ + if (enabled) + m_hexRows.insert(row); + else + m_hexRows.remove(row); + + QModelIndex idx = index(row, 1); + emit dataChanged(idx, idx, {Qt::DisplayRole}); +} + +bool KeyValueModel::loadValueFromFile(int row, const QByteArray &fileData) +{ + ensureCached(row); + int localIdx = row - m_cacheStartRow; + if (localIdx < 0 || localIdx >= m_cache.size()) + return false; + + QByteArray key = m_cache[localIdx].key; + if (m_ops.putValue && m_ops.putValue(key, fileData)) { + m_cache[localIdx].value = fileData; + QModelIndex idx = index(row, 1); + emit dataChanged(idx, idx, {Qt::DisplayRole, Qt::EditRole}); + return true; + } + return false; +} + +bool KeyValueModel::isValidUtf8(const QByteArray &data) +{ + QStringDecoder decoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless); + QString result = decoder(data); + Q_UNUSED(result); + return !decoder.hasError(); +} + +QString KeyValueModel::formatBinaryPlaceholder(int size) +{ + return QStringLiteral("[Binary Data - %1 bytes]").arg(size); +} + +QString KeyValueModel::toHexString(const QByteArray &data) +{ + return QString::fromLatin1(data.toHex(' ')); +} diff --git a/wlx/dbview/src/LevelDbEngine.cpp b/wlx/dbview/src/LevelDbEngine.cpp new file mode 100644 index 0000000..b55e092 --- /dev/null +++ b/wlx/dbview/src/LevelDbEngine.cpp @@ -0,0 +1,142 @@ +#ifdef ENABLE_ROCKSDB_LEVELDB + +#include "LevelDbEngine.h" +#include "KeyValueModel.h" + +#include +#include + +#include +#include + +namespace { +class RocksDbNoOpLogger : public rocksdb::Logger { +public: + using rocksdb::Logger::Logv; + void Logv(const char* format, va_list ap) override { + Q_UNUSED(format); + Q_UNUSED(ap); + } +}; +} + +LevelDbEngine::LevelDbEngine(QObject *parent) + : DbEngine(parent) +{ +} + +LevelDbEngine::~LevelDbEngine() +{ + close(); +} + +bool LevelDbEngine::open(const QString &filepath) +{ + close(); + + QFileInfo info(filepath); + QString dbPath = info.isDir() ? filepath : info.absolutePath(); + + if (!QFile::exists(dbPath + QStringLiteral("/CURRENT"))) + return false; + + rocksdb::Options options; + options.create_if_missing = false; + options.info_log = std::make_shared(); + + rocksdb::DB *db = nullptr; + rocksdb::Status status = rocksdb::DB::OpenForReadOnly(options, dbPath.toStdString(), &db); + if (!status.ok() || !db) + return false; + + m_db = db; + m_readOnly = true; + m_keyCount = countKeys(); + return true; +} + +void LevelDbEngine::close() +{ + delete m_model; + m_model = nullptr; + + delete m_db; + m_db = nullptr; + + m_keyCount = 0; +} + +int LevelDbEngine::countKeys() const +{ + if (!m_db) return 0; + + int count = 0; + rocksdb::Iterator *it = m_db->NewIterator(rocksdb::ReadOptions()); + for (it->SeekToFirst(); it->Valid(); it->Next()) + ++count; + delete it; + return count; +} + +QStringList LevelDbEngine::tableNames() const +{ + return { QStringLiteral("") }; +} + +QString LevelDbEngine::currentTableName() const +{ + return QStringLiteral(""); +} + +QAbstractItemModel *LevelDbEngine::modelForTable(const QString &tableName) +{ + Q_UNUSED(tableName); + if (!m_db) return nullptr; + + delete m_model; + m_model = nullptr; + + rocksdb::DB *db = m_db; + + KeyValueModel::IteratorOps ops; + + ops.fetchWindow = [db](int startIndex, int count) -> QVector { + QVector entries; + entries.reserve(count); + + rocksdb::Iterator *it = db->NewIterator(rocksdb::ReadOptions()); + it->SeekToFirst(); + + for (int i = 0; i < startIndex && it->Valid(); ++i) + it->Next(); + + for (int i = 0; i < count && it->Valid(); ++i) { + KeyValueModel::Entry e; + e.key = QByteArray(it->key().data(), static_cast(it->key().size())); + e.value = QByteArray(it->value().data(), static_cast(it->value().size())); + entries.append(std::move(e)); + it->Next(); + } + + delete it; + return entries; + }; + + ops.putValue = [db](const QByteArray &key, const QByteArray &value) -> bool { + rocksdb::Slice k(key.constData(), key.size()); + rocksdb::Slice v(value.constData(), value.size()); + rocksdb::Status s = db->Put(rocksdb::WriteOptions(), k, v); + return s.ok(); + }; + + ops.deleteKey = [db](const QByteArray &key) -> bool { + rocksdb::Slice k(key.constData(), key.size()); + rocksdb::Status s = db->Delete(rocksdb::WriteOptions(), k); + return s.ok(); + }; + + m_model = new KeyValueModel(m_keyCount, std::move(ops), this); + return m_model; +} + +#endif // ENABLE_ROCKSDB_LEVELDB diff --git a/wlx/dbview/src/LmdbEngine.cpp b/wlx/dbview/src/LmdbEngine.cpp new file mode 100644 index 0000000..9945c53 --- /dev/null +++ b/wlx/dbview/src/LmdbEngine.cpp @@ -0,0 +1,227 @@ +#include "LmdbEngine.h" +#include "KeyValueModel.h" + +#include +#include + +LmdbEngine::LmdbEngine(QObject *parent) + : DbEngine(parent) +{ +} + +LmdbEngine::~LmdbEngine() +{ + close(); +} + +bool LmdbEngine::open(const QString &filepath) +{ + close(); + + int rc = mdb_env_create(&m_env); + if (rc != 0) return false; + + // Set large map size + mdb_env_set_mapsize(m_env, 104857600); // 100MB + + QFileInfo info(filepath); + QString dbPath; + unsigned int flags = 0; + if (info.isDir()) { + dbPath = filepath; + } else { + if (info.fileName() == QStringLiteral("data.mdb")) { + dbPath = info.absolutePath(); + } else { + dbPath = filepath; + flags |= MDB_NOSUBDIR; + } + } + + // Try read-write + rc = mdb_env_open(m_env, dbPath.toLocal8Bit().constData(), flags, 0664); + if (rc != 0) { + // Fallback to read-only + rc = mdb_env_open(m_env, dbPath.toLocal8Bit().constData(), flags | MDB_RDONLY, 0); + if (rc != 0) { + mdb_env_close(m_env); + m_env = nullptr; + return false; + } + m_readOnly = true; + } else { + m_readOnly = false; + } + + // Open transaction to open DBI database + MDB_txn *txn = nullptr; + rc = mdb_txn_begin(m_env, nullptr, m_readOnly ? MDB_RDONLY : 0, &txn); + if (rc != 0) { + close(); + return false; + } + + rc = mdb_dbi_open(txn, nullptr, 0, &m_dbi); + if (rc != 0) { + mdb_txn_abort(txn); + close(); + return false; + } + + mdb_txn_commit(txn); + + m_keyCount = countKeys(); + return true; +} + +void LmdbEngine::close() +{ + delete m_model; + m_model = nullptr; + + if (m_env) { + // DBI is closed automatically when environment is closed + mdb_env_close(m_env); + m_env = nullptr; + m_dbi = 0; + } + m_keyCount = 0; +} + +int LmdbEngine::countKeys() const +{ + if (!m_env) return 0; + + MDB_txn *txn = nullptr; + int rc = mdb_txn_begin(m_env, nullptr, MDB_RDONLY, &txn); + if (rc != 0) return 0; + + MDB_cursor *cursor = nullptr; + rc = mdb_cursor_open(txn, m_dbi, &cursor); + if (rc != 0) { + mdb_txn_abort(txn); + return 0; + } + + int count = 0; + MDB_val key, data; + while (mdb_cursor_get(cursor, &key, &data, MDB_NEXT) == 0) { + count++; + } + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + return count; +} + +QStringList LmdbEngine::tableNames() const +{ + return { QStringLiteral("") }; +} + +QString LmdbEngine::currentTableName() const +{ + return QStringLiteral(""); +} + +QAbstractItemModel *LmdbEngine::modelForTable(const QString &tableName) +{ + Q_UNUSED(tableName); + if (!m_env) return nullptr; + + delete m_model; + m_model = nullptr; + + KeyValueModel::IteratorOps ops; + + ops.fetchWindow = [this](int startIndex, int count) -> QVector { + QVector entries; + if (!m_env) return entries; + entries.reserve(count); + + MDB_txn *txn = nullptr; + int rc = mdb_txn_begin(m_env, nullptr, MDB_RDONLY, &txn); + if (rc != 0) return entries; + + MDB_cursor *cursor = nullptr; + rc = mdb_cursor_open(txn, m_dbi, &cursor); + if (rc != 0) { + mdb_txn_abort(txn); + return entries; + } + + MDB_val key, data; + rc = mdb_cursor_get(cursor, &key, &data, MDB_FIRST); + if (rc == 0) { + for (int i = 0; i < startIndex; ++i) { + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + if (rc != 0) break; + } + if (rc == 0) { + for (int i = 0; i < count; ++i) { + KeyValueModel::Entry e; + e.key = QByteArray(static_cast(key.mv_data), static_cast(key.mv_size)); + e.value = QByteArray(static_cast(data.mv_data), static_cast(data.mv_size)); + entries.append(std::move(e)); + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + if (rc != 0) break; + } + } + } + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + return entries; + }; + + ops.putValue = [this](const QByteArray &key, const QByteArray &value) -> bool { + if (!m_env || m_readOnly) return false; + + MDB_txn *txn = nullptr; + int rc = mdb_txn_begin(m_env, nullptr, 0, &txn); + if (rc != 0) return false; + + MDB_val k, v; + k.mv_data = (void*)key.constData(); + k.mv_size = key.size(); + v.mv_data = (void*)value.constData(); + v.mv_size = value.size(); + + rc = mdb_put(txn, m_dbi, &k, &v, 0); + if (rc != 0) { + mdb_txn_abort(txn); + return false; + } + + rc = mdb_txn_commit(txn); + return rc == 0; + }; + + ops.deleteKey = [this](const QByteArray &key) -> bool { + if (!m_env || m_readOnly) return false; + + MDB_txn *txn = nullptr; + int rc = mdb_txn_begin(m_env, nullptr, 0, &txn); + if (rc != 0) return false; + + MDB_val k; + k.mv_data = (void*)key.constData(); + k.mv_size = key.size(); + + rc = mdb_del(txn, m_dbi, &k, nullptr); + if (rc != 0) { + mdb_txn_abort(txn); + return false; + } + + rc = mdb_txn_commit(txn); + if (rc == 0) { + m_keyCount--; + return true; + } + return false; + }; + + m_model = new KeyValueModel(m_keyCount, std::move(ops), this); + return m_model; +} diff --git a/wlx/dbview/src/MdbEngine.cpp b/wlx/dbview/src/MdbEngine.cpp new file mode 100644 index 0000000..b408a76 --- /dev/null +++ b/wlx/dbview/src/MdbEngine.cpp @@ -0,0 +1,303 @@ +#include "MdbEngine.h" + +#include +#include + +MdbModel::MdbModel(MdbEngine *engine, MdbTableDef *table, QObject *parent) + : QAbstractTableModel(parent) + , m_engine(engine) + , m_table(table) +{ + // Retrieve columns + mdb_read_columns(m_table); + for (unsigned int i = 0; i < m_table->num_cols; ++i) { + MdbColumn *col = (MdbColumn *)g_ptr_array_index(m_table->columns, i); + m_columnNames.append(QString::fromUtf8(col->name)); + m_columnTypes.append(col->col_type); + + // Allocate bind buffers for catalog/data scanning + col->bind_ptr = malloc(MDB_BIND_SIZE); + col->len_ptr = (int *)malloc(sizeof(int)); + memset(col->bind_ptr, 0, MDB_BIND_SIZE); + *col->len_ptr = 0; + } +} + +MdbModel::~MdbModel() +{ + if (m_table) { + for (unsigned int i = 0; i < m_table->num_cols; ++i) { + MdbColumn *col = (MdbColumn *)g_ptr_array_index(m_table->columns, i); + free(col->bind_ptr); + free(col->len_ptr); + col->bind_ptr = nullptr; + col->len_ptr = nullptr; + } + mdb_free_tabledef(m_table); + m_table = nullptr; + } +} + +int MdbModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_data.size(); +} + +int MdbModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_columnNames.size(); +} + +QVariant MdbModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_data.size() || index.column() >= m_columnNames.size()) + return {}; + + if (role == Qt::DisplayRole || role == Qt::EditRole) { + if (isBinaryValue(index.row(), index.column())) { + int size = m_rawData[index.row()][index.column()].size(); + return QStringLiteral("[Binary Data - %1 bytes]").arg(size); + } + return m_data[index.row()][index.column()]; + } + return {}; +} + +QVariant MdbModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return {}; + if (section < 0 || section >= m_columnNames.size()) + return {}; + return m_columnNames[section]; +} + +Qt::ItemFlags MdbModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + // MS Access is read-only + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +bool MdbModel::isBinaryValue(int row, int col) const +{ + if (row < 0 || row >= m_columnTypes.size() || col < 0 || col >= m_columnTypes.size()) + return false; + int type = m_columnTypes[col]; + return (type == MDB_OLE || type == MDB_BINARY); +} + +QByteArray MdbModel::rawValue(int row, int col) const +{ + if (row < 0 || row >= m_rawData.size() || col < 0 || col >= m_columnNames.size()) + return {}; + return m_rawData[row][col]; +} + +bool MdbModel::select() +{ + beginResetModel(); + m_data.clear(); + m_rawData.clear(); + + if (!m_table || !m_engine->handle()) { + endResetModel(); + return false; + } + + mdb_rewind_table(m_table); + while (mdb_fetch_row(m_table)) { + QVector rowData; + QVector rawRowData; + rowData.reserve(m_table->num_cols); + rawRowData.reserve(m_table->num_cols); + + for (unsigned int i = 0; i < m_table->num_cols; ++i) { + MdbColumn *col = (MdbColumn *)g_ptr_array_index(m_table->columns, i); + + if (col->col_type == MDB_OLE) { + size_t size = 0; + void *ptr = mdb_ole_read_full(m_engine->handle(), col, &size); + QByteArray bytes; + if (ptr) { + bytes = QByteArray((const char*)ptr, size); + free(ptr); + } + rowData.append(QVariant()); + rawRowData.append(bytes); + } else if (col->col_type == MDB_BINARY) { + QByteArray bytes; + if (col->cur_value_len > 0) { + bytes = QByteArray((const char*)m_engine->handle()->pg_buf + col->cur_value_start, col->cur_value_len); + } + rowData.append(QVariant()); + rawRowData.append(bytes); + } else { + QString val = QString::fromUtf8((const char*)col->bind_ptr); + rowData.append(val); + rawRowData.append(QByteArray()); + } + } + m_data.append(std::move(rowData)); + m_rawData.append(std::move(rawRowData)); + } + + endResetModel(); + return true; +} + +MdbEngine::MdbEngine(QObject *parent) + : DbEngine(parent) +{ +} + +MdbEngine::~MdbEngine() +{ + close(); +} + +bool MdbEngine::open(const QString &filepath) +{ + close(); + + // MS Access is always read-only + m_mdb = mdb_open(filepath.toLocal8Bit().constData(), MDB_NOFLAGS); + if (!m_mdb) { + return false; + } + + m_readOnly = true; + return true; +} + +void MdbEngine::close() +{ + delete m_currentModel; + m_currentModel = nullptr; + + if (m_mdb) { + mdb_close(m_mdb); + m_mdb = nullptr; + } + m_currentTable.clear(); +} + +QStringList MdbEngine::tableNames() const +{ + QStringList result; + if (!m_mdb) return result; + + mdb_read_catalog(m_mdb, MDB_TABLE); + for (unsigned int i = 0; i < m_mdb->num_catalog; ++i) { + MdbCatalogEntry *entry = (MdbCatalogEntry *)g_ptr_array_index(m_mdb->catalog, i); + if (entry->object_type == MDB_TABLE) { + if (mdb_is_user_table(entry)) { + result.append(QString::fromUtf8(entry->object_name)); + } + } + } + result.sort(); + return result; +} + +QList MdbEngine::columnInfos(const QString &tableName) const +{ + QList result; + if (!m_mdb) return result; + + MdbCatalogEntry *entry = mdb_get_catalogentry_by_name(m_mdb, tableName.toLocal8Bit().constData()); + if (!entry) return result; + + MdbTableDef *table = mdb_read_table(entry); + if (!table) return result; + + mdb_read_columns(table); + for (unsigned int i = 0; i < table->num_cols; ++i) { + MdbColumn *col = (MdbColumn *)g_ptr_array_index(table->columns, i); + ColumnInfo info; + info.name = QString::fromUtf8(col->name); + info.type = QString::fromLatin1(mdb_get_colbacktype_string(col)); + if (col->col_type == MDB_TEXT) { + info.type += QStringLiteral("(%1)").arg(col->col_size); + } + // libmdb does not easily expose primary keys unless we read relationships/indices. + // We can check indexes below. + result.append(info); + } + + // Try to tag primary key columns if there's an index called "PrimaryKey" + GPtrArray *indices = mdb_read_indices(table); + if (indices) { + for (unsigned int i = 0; i < indices->len; ++i) { + MdbIndex *idx = (MdbIndex *)g_ptr_array_index(indices, i); + if (strcmp(idx->name, "PrimaryKey") == 0) { + for (int k = 0; k < idx->num_keys; ++k) { + int colIdx = idx->key_col_num[k] - 1; + if (colIdx >= 0 && colIdx < result.size()) { + result[colIdx].isPrimaryKey = true; + } + } + } + } + mdb_free_indices(indices); + } + + mdb_free_tabledef(table); + return result; +} + +QStringList MdbEngine::indexes(const QString &tableName) const +{ + QStringList result; + if (!m_mdb) return result; + + MdbCatalogEntry *entry = mdb_get_catalogentry_by_name(m_mdb, tableName.toLocal8Bit().constData()); + if (!entry) return result; + + MdbTableDef *table = mdb_read_table(entry); + if (!table) return result; + + mdb_read_columns(table); + + GPtrArray *indices = mdb_read_indices(table); + if (indices) { + for (unsigned int i = 0; i < indices->len; ++i) { + MdbIndex *idx = (MdbIndex *)g_ptr_array_index(indices, i); + result.append(QString::fromUtf8(idx->name)); + } + mdb_free_indices(indices); + } + + mdb_free_tabledef(table); + return result; +} + +QAbstractItemModel *MdbEngine::modelForTable(const QString &tableName) +{ + if (!m_mdb) return nullptr; + + delete m_currentModel; + m_currentModel = nullptr; + m_currentTable = tableName; + + MdbCatalogEntry *entry = mdb_get_catalogentry_by_name(m_mdb, tableName.toLocal8Bit().constData()); + if (!entry) return nullptr; + + MdbTableDef *table = mdb_read_table(entry); + if (!table) return nullptr; + + auto *model = new MdbModel(this, table, this); + if (!model->select()) { + delete model; + return nullptr; + } + + m_currentModel = model; + return model; +} + +QString MdbEngine::currentTableName() const +{ + return m_currentTable; +} diff --git a/wlx/dbview/src/RocksDbEngine.cpp b/wlx/dbview/src/RocksDbEngine.cpp new file mode 100644 index 0000000..c6b784a --- /dev/null +++ b/wlx/dbview/src/RocksDbEngine.cpp @@ -0,0 +1,142 @@ +#ifdef ENABLE_ROCKSDB_LEVELDB + +#include "RocksDbEngine.h" +#include "KeyValueModel.h" + +#include +#include + +#include +#include + +namespace { +class RocksDbNoOpLogger : public rocksdb::Logger { +public: + using rocksdb::Logger::Logv; + void Logv(const char* format, va_list ap) override { + Q_UNUSED(format); + Q_UNUSED(ap); + } +}; +} + +RocksDbEngine::RocksDbEngine(QObject *parent) + : DbEngine(parent) +{ +} + +RocksDbEngine::~RocksDbEngine() +{ + close(); +} + +bool RocksDbEngine::open(const QString &filepath) +{ + close(); + + QFileInfo info(filepath); + QString dbPath = info.isDir() ? filepath : info.absolutePath(); + + if (!QFile::exists(dbPath + QStringLiteral("/CURRENT"))) + return false; + + rocksdb::Options options; + options.create_if_missing = false; + options.info_log = std::make_shared(); + + rocksdb::DB *db = nullptr; + rocksdb::Status status = rocksdb::DB::OpenForReadOnly(options, dbPath.toStdString(), &db); + if (!status.ok() || !db) + return false; + + m_db = db; + m_readOnly = true; + m_keyCount = countKeys(); + return true; +} + +void RocksDbEngine::close() +{ + delete m_model; + m_model = nullptr; + + delete m_db; + m_db = nullptr; + + m_keyCount = 0; +} + +int RocksDbEngine::countKeys() const +{ + if (!m_db) return 0; + + int count = 0; + rocksdb::Iterator *it = m_db->NewIterator(rocksdb::ReadOptions()); + for (it->SeekToFirst(); it->Valid(); it->Next()) + ++count; + delete it; + return count; +} + +QStringList RocksDbEngine::tableNames() const +{ + return { QStringLiteral("") }; +} + +QString RocksDbEngine::currentTableName() const +{ + return QStringLiteral(""); +} + +QAbstractItemModel *RocksDbEngine::modelForTable(const QString &tableName) +{ + Q_UNUSED(tableName); + if (!m_db) return nullptr; + + delete m_model; + m_model = nullptr; + + rocksdb::DB *db = m_db; + + KeyValueModel::IteratorOps ops; + + ops.fetchWindow = [db](int startIndex, int count) -> QVector { + QVector entries; + entries.reserve(count); + + rocksdb::Iterator *it = db->NewIterator(rocksdb::ReadOptions()); + it->SeekToFirst(); + + for (int i = 0; i < startIndex && it->Valid(); ++i) + it->Next(); + + for (int i = 0; i < count && it->Valid(); ++i) { + KeyValueModel::Entry e; + e.key = QByteArray(it->key().data(), static_cast(it->key().size())); + e.value = QByteArray(it->value().data(), static_cast(it->value().size())); + entries.append(std::move(e)); + it->Next(); + } + + delete it; + return entries; + }; + + ops.putValue = [db](const QByteArray &key, const QByteArray &value) -> bool { + rocksdb::Slice k(key.constData(), key.size()); + rocksdb::Slice v(value.constData(), value.size()); + rocksdb::Status s = db->Put(rocksdb::WriteOptions(), k, v); + return s.ok(); + }; + + ops.deleteKey = [db](const QByteArray &key) -> bool { + rocksdb::Slice k(key.constData(), key.size()); + rocksdb::Status s = db->Delete(rocksdb::WriteOptions(), k); + return s.ok(); + }; + + m_model = new KeyValueModel(m_keyCount, std::move(ops), this); + return m_model; +} + +#endif // ENABLE_ROCKSDB_LEVELDB diff --git a/wlx/dbview/src/SqliteEngine.cpp b/wlx/dbview/src/SqliteEngine.cpp new file mode 100644 index 0000000..f33e871 --- /dev/null +++ b/wlx/dbview/src/SqliteEngine.cpp @@ -0,0 +1,187 @@ +#include "SqliteEngine.h" + +#include +#include +#include +#include +#include + +SqliteEngine::SqliteEngine(QObject *parent) + : DbEngine(parent) + , m_connectionName(QUuid::createUuid().toString(QUuid::WithoutBraces)) +{ +} + +SqliteEngine::~SqliteEngine() +{ + close(); +} + +bool SqliteEngine::open(const QString &filepath) +{ + close(); + + m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName); + m_db.setDatabaseName(filepath); + + if (!m_db.open()) + return false; + + return true; +} + +void SqliteEngine::close() +{ + if (m_currentModel) { + delete m_currentModel; + m_currentModel = nullptr; + } + m_currentTable.clear(); + + if (m_db.isOpen()) + m_db.close(); + + // Release the QSqlDatabase reference BEFORE removing the connection, + // otherwise Qt warns "connection still in use". + QString connName = m_connectionName; + m_db = QSqlDatabase(); + + if (QSqlDatabase::connectionNames().contains(connName)) + QSqlDatabase::removeDatabase(connName); +} + +QStringList SqliteEngine::tableNames() const +{ + if (!m_db.isOpen()) + return {}; + return m_db.tables(QSql::Tables); +} + +QStringList SqliteEngine::viewNames() const +{ + if (!m_db.isOpen()) + return {}; + return m_db.tables(QSql::Views); +} + +QList SqliteEngine::columnInfos(const QString &tableName) const +{ + QList result; + if (!m_db.isOpen()) return result; + + QSqlQuery q(m_db); + q.prepare(QStringLiteral("PRAGMA table_info(\"%1\")").arg(tableName)); + if (q.exec()) { + while (q.next()) { + ColumnInfo info; + info.name = q.value(1).toString(); + info.type = q.value(2).toString(); + if (info.type.isEmpty()) { + info.type = QStringLiteral("VARIANT"); + } + int pk = q.value(5).toInt(); + info.isPrimaryKey = (pk > 0); + result.append(info); + } + } + return result; +} + +QStringList SqliteEngine::indexes(const QString &tableName) const +{ + QStringList result; + if (!m_db.isOpen()) return result; + + QSqlQuery q(m_db); + q.prepare(QStringLiteral("PRAGMA index_list(\"%1\")").arg(tableName)); + if (q.exec()) { + while (q.next()) { + result.append(q.value(1).toString()); + } + } + return result; +} + +QAbstractItemModel *SqliteEngine::modelForTable(const QString &tableName) +{ + if (!m_db.isOpen()) + return nullptr; + + if (m_currentModel) { + delete m_currentModel; + m_currentModel = nullptr; + } + + m_currentTable = tableName; + + auto *model = new QSqlTableModel(this, m_db); + model->setTable(tableName); + model->setEditStrategy(QSqlTableModel::OnManualSubmit); + model->select(); + + while (model->canFetchMore()) + model->fetchMore(); + + m_currentModel = model; + return model; +} + +QString SqliteEngine::currentTableName() const +{ + return m_currentTable; +} + +bool SqliteEngine::submitAll() +{ + if (!m_currentModel) return false; + if (!m_currentModel->submitAll()) + return false; + return true; +} + +bool SqliteEngine::revertAll() +{ + if (!m_currentModel) return false; + m_currentModel->revertAll(); + return true; +} + +QAbstractItemModel *SqliteEngine::executeQuery(const QString &query) +{ + if (!m_db.isOpen()) return nullptr; + + m_lastQueryError = false; + m_lastError.clear(); + + QSqlQuery q(m_db); + if (!q.exec(query)) { + m_lastQueryError = true; + m_lastError = q.lastError().text(); + return nullptr; + } + + if (q.isSelect()) { + m_lastQueryError = false; + auto *model = new QSqlQueryModel(this); + model->setQuery(std::move(q)); + return model; + } else { + m_lastQueryError = false; + int affected = q.numRowsAffected(); + if (affected < 0) { + m_lastError = QStringLiteral("Query executed successfully."); + } else { + m_lastError = QStringLiteral("Query executed successfully, %1 row(s) affected.").arg(affected); + } + return nullptr; + } +} + +QString SqliteEngine::lastError() const +{ + if (m_lastQueryError || !m_lastError.isEmpty()) + return m_lastError; + if (m_currentModel) + return m_currentModel->lastError().text(); + return m_db.lastError().text(); +} diff --git a/wlx/dbview/src/libmdb/backend.c b/wlx/dbview/src/libmdb/backend.c new file mode 100644 index 0000000..0f2432b --- /dev/null +++ b/wlx/dbview/src/libmdb/backend.c @@ -0,0 +1,1219 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2000-2011 Brian Bruns and others + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* +** functions to deal with different backend database engines +*/ + +#include "mdbtools.h" +#include "mdbprivate.h" + +/* Access data types */ +static const MdbBackendType mdb_access_types[] = { + [MDB_BOOL] = { .name = "Boolean" }, + [MDB_BYTE] = { .name = "Byte" }, + [MDB_INT] = { .name = "Integer" }, + [MDB_LONGINT] = { .name = "Long Integer" }, + [MDB_MONEY] = { .name = "Currency" }, + [MDB_FLOAT] = { .name = "Single" }, + [MDB_DOUBLE] = { .name = "Double" }, + [MDB_DATETIME] = { .name = "DateTime" }, + [MDB_BINARY] = { .name = "Binary" }, + [MDB_TEXT] = { .name = "Text", .needs_char_length = 1 }, + [MDB_OLE] = { .name = "OLE", .needs_byte_length = 1 }, + [MDB_MEMO] = { .name = "Memo/Hyperlink", .needs_char_length = 1 }, + [MDB_REPID] = { .name = "Replication ID" }, + [MDB_NUMERIC] = { .name = "Numeric", .needs_precision = 1, .needs_scale = 1 }, +}; + +/* Oracle data types */ +static const MdbBackendType mdb_oracle_types[] = { + [MDB_BOOL] = { .name = "NUMBER(1)" }, + [MDB_BYTE] = { .name = "NUMBER(3)" }, + [MDB_INT] = { .name = "NUMBER(5)" }, + [MDB_LONGINT] = { .name = "NUMBER(11)" }, + [MDB_MONEY] = { .name = "NUMBER(15,2)" }, + [MDB_FLOAT] = { .name = "FLOAT" }, + [MDB_DOUBLE] = { .name = "FLOAT" }, + [MDB_DATETIME] = { .name = "TIMESTAMP" }, + [MDB_BINARY] = { .name = "BINARY" }, + [MDB_TEXT] = { .name = "VARCHAR2", .needs_char_length= 1 }, + [MDB_OLE] = { .name = "BLOB" }, + [MDB_MEMO] = { .name = "CLOB" }, + [MDB_REPID] = { .name = "NUMBER", .needs_precision = 1 }, + [MDB_NUMERIC] = { .name = "NUMBER", .needs_precision = 1 }, +}; +static const MdbBackendType mdb_oracle_shortdate_type = + { .name = "DATE" }; + +/* Sybase/MSSQL data types */ +static const MdbBackendType mdb_sybase_types[] = { + [MDB_BOOL] = { .name = "bit" }, + [MDB_BYTE] = { .name = "char", .needs_byte_length = 1 }, + [MDB_INT] = { .name = "smallint" }, + [MDB_LONGINT] = { .name = "int" }, + [MDB_MONEY] = { .name = "money" }, + [MDB_FLOAT] = { .name = "real" }, + [MDB_DOUBLE] = { .name = "float" }, + [MDB_DATETIME] = { .name = "smalldatetime" }, + [MDB_BINARY] = { .name = "varbinary", .needs_byte_length = 1 }, + [MDB_TEXT] = { .name = "nvarchar", .needs_char_length = 1 }, + [MDB_OLE] = { .name = "varbinary(max)" }, + [MDB_MEMO] = { .name = "nvarchar(max)" }, + [MDB_REPID] = { .name = "Sybase_Replication ID" }, + [MDB_NUMERIC] = { .name = "numeric", .needs_precision = 1, .needs_scale = 1 }, +}; +static const MdbBackendType mdb_sybase_shortdate_type = + { .name = "DATE" }; + +/* Postgres data types */ +static const MdbBackendType mdb_postgres_types[] = { + [MDB_BOOL] = { .name = "BOOLEAN" }, + [MDB_BYTE] = { .name = "SMALLINT" }, + [MDB_INT] = { .name = "INTEGER" }, + [MDB_LONGINT] = { .name = "INTEGER" }, /* bigint */ + [MDB_MONEY] = { .name = "NUMERIC(15,2)" }, /* money deprecated ? */ + [MDB_FLOAT] = { .name = "REAL" }, + [MDB_DOUBLE] = { .name = "DOUBLE PRECISION" }, + [MDB_DATETIME] = { .name = "TIMESTAMP WITHOUT TIME ZONE" }, + [MDB_BINARY] = { .name = "BYTEA" }, + [MDB_TEXT] = { .name = "VARCHAR", .needs_char_length = 1 }, + [MDB_OLE] = { .name = "BYTEA" }, + [MDB_MEMO] = { .name = "TEXT" }, + [MDB_REPID] = { .name = "UUID" }, + [MDB_NUMERIC] = { .name = "NUMERIC", .needs_precision = 1, .needs_scale = 1 }, +}; +static const MdbBackendType mdb_postgres_shortdate_type = + { .name = "DATE" }; +static const MdbBackendType mdb_postgres_serial_type = + { .name = "SERIAL" }; + +/* MySQL data types */ +static const MdbBackendType mdb_mysql_types[] = { + [MDB_BOOL] = { .name = "boolean" }, + [MDB_BYTE] = { .name = "tinyint" }, + [MDB_INT] = { .name = "smallint" }, + [MDB_LONGINT] = { .name = "int" }, + [MDB_MONEY] = { .name = "float" }, + [MDB_FLOAT] = { .name = "float" }, + [MDB_DOUBLE] = { .name = "double" }, + [MDB_DATETIME] = { .name = "datetime" }, + [MDB_BINARY] = { .name = "blob" }, + [MDB_TEXT] = { .name = "varchar", .needs_char_length = 1 }, + [MDB_OLE] = { .name = "blob" }, + [MDB_MEMO] = { .name = "text" }, + [MDB_REPID] = { .name = "char(38)" }, + [MDB_NUMERIC] = { .name = "numeric", .needs_precision = 1, .needs_scale = 1 }, +}; +static const MdbBackendType mdb_mysql_shortdate_type = + { .name = "date" }; +/* We can't use the MySQL SERIAL type because that uses a bigint which + * is 64 bits wide, whereas MDB long ints are 32 bits */ +static const MdbBackendType mdb_mysql_serial_type = + { .name = "int not null auto_increment unique" }; + +/* sqlite data types */ +static const MdbBackendType mdb_sqlite_types[] = { + [MDB_BOOL] = { .name = "INTEGER" }, + [MDB_BYTE] = { .name = "INTEGER" }, + [MDB_INT] = { .name = "INTEGER" }, + [MDB_LONGINT] = { .name = "INTEGER" }, + [MDB_MONEY] = { .name = "REAL" }, + [MDB_FLOAT] = { .name = "REAL" }, + [MDB_DOUBLE] = { .name = "REAL" }, + [MDB_DATETIME] = { .name = "DateTime" }, + [MDB_BINARY] = { .name = "BLOB" }, + [MDB_TEXT] = { .name = "varchar" }, + [MDB_OLE] = { .name = "BLOB" }, + [MDB_MEMO] = { .name = "TEXT" }, + [MDB_REPID] = { .name = "INTEGER" }, + [MDB_NUMERIC] = { .name = "INTEGER" }, +}; + +enum { + MDB_BACKEND_ACCESS = 1, + MDB_BACKEND_ORACLE, + MDB_BACKEND_SYBASE, + MDB_BACKEND_POSTGRES, + MDB_BACKEND_MYSQL, + MDB_BACKEND_SQLITE, +}; + +static void mdb_drop_backend(gpointer key, gpointer value, gpointer data); + + +static gchar *passthrough_unchanged(const gchar *str) { + return (gchar *)str; +} + +static gchar *to_lower_case(const gchar *str) { + return g_utf8_strdown(str, -1); +} + +/** + * Convenience function to replace an input string with its database specific normalised version. + * + * This function throws away the input string after normalisation, freeing its memory, and replaces it with a new + * normalised version allocated on the stack. + * + * @param mdb Database specific MDB handle containing pointers to utility methods + * @param str string to normalise + * @return a pointer to the normalised version of the input string + */ +gchar *mdb_normalise_and_replace(MdbHandle *mdb, gchar **str) { + gchar *normalised_str = mdb->default_backend->normalise_case(*str); + if (normalised_str != *str) { + /* Free and replace the old string only and only if a new string was created at a different memory location + * so that we can account for the case where strings a just passed through unchanged. + */ + free(*str); + *str = normalised_str; + } + return *str; +} + +static gchar* +quote_generic(const gchar *value, gchar quote_char, gchar escape_char) { + gchar *result, *pr; + unsigned char c; + + pr = result = g_malloc(1+4*strlen(value)+2); // worst case scenario + + *pr++ = quote_char; + while ((c=*(unsigned char*)value++)) { + if (c<32) { + sprintf(pr, "\\%03o", c); + pr+=4; + continue; + } + else if (c == quote_char) { + *pr++ = escape_char; + } + *pr++ = c; + } + *pr++ = quote_char; + *pr++ = '\0'; + return result; +} +static gchar* +quote_schema_name_bracket_merge(const gchar* schema, const gchar *name) { + if (schema) + return g_strconcat("[", schema, "_", name, "]", NULL); + else + return g_strconcat("[", name, "]", NULL); +} + +/* + * For backends that really does support schema + * returns "name" or "schema"."name" + */ +static gchar* +quote_schema_name_dquote(const gchar* schema, const gchar *name) +{ + if (schema) { + gchar *frag1 = quote_generic(schema, '"', '"'); + gchar *frag2 = quote_generic(name, '"', '"'); + gchar *result = g_strconcat(frag1, ".", frag2, NULL); + g_free(frag1); + g_free(frag2); + return result; + } + return quote_generic(name, '"', '"'); +} + +/* + * For backends that really do NOT support schema + * returns "name" or "schema_name" + */ +/* +static gchar* +quote_schema_name_dquote_merge(const gchar* schema, const gchar *name) +{ + if (schema) { + gchar *combined = g_strconcat(schema, "_", name, NULL); + gchar *result = quote_generic(combined, '"', '"'); + g_free(combined); + return result; + } + return quote_generic(name, '"', '"'); +}*/ + +static gchar* +quote_schema_name_rquotes_merge(const gchar* schema, const gchar *name) +{ + if (schema) { + gchar *combined = g_strconcat(schema, "_", name, NULL); + gchar *result = quote_generic(combined, '`', '`'); + g_free(combined); + return result; + } + return quote_generic(name, '`', '`'); +} + +static gchar* +quote_with_squotes(const gchar* value) +{ + return quote_generic(value, '\'', '\''); +} + +const MdbBackendType* +mdb_get_colbacktype(const MdbColumn *col) { + MdbBackend *backend = col->table->entry->mdb->default_backend; + int col_type = col->col_type; + if (col_type > MDB_NUMERIC) + return NULL; + if (col_type == MDB_LONGINT && col->is_long_auto && backend->type_autonum) + return backend->type_autonum; + if (col_type == MDB_DATETIME && backend->type_shortdate) { + if (mdb_col_is_shortdate(col)) + return backend->type_shortdate; + } + if (!backend->types_table[col_type].name[0]) + return NULL; + return &backend->types_table[col_type]; +} + +const char * +mdb_get_colbacktype_string(const MdbColumn *col) +{ + const MdbBackendType *type = mdb_get_colbacktype(col); + if (!type) { + // return NULL; + static TLS char buf[16]; + snprintf(buf, sizeof(buf), "Unknown_%04x", col->col_type); + return buf; + } + return type->name; +} +int +mdb_colbacktype_takes_length(const MdbColumn *col) +{ + const MdbBackendType *type = mdb_get_colbacktype(col); + if (!type) return 0; + return type->needs_precision || type->needs_char_length || type->needs_byte_length; +} +static int +mdb_colbacktype_takes_length_in_characters(const MdbColumn *col) +{ + const MdbBackendType *type = mdb_get_colbacktype(col); + if (!type) return 0; + return type->needs_char_length; +} + +/** + * mdb_init_backends + * + * Initializes the mdb_backends hash and loads the builtin backends. + * Use mdb_remove_backends() to destroy this hash when done. + */ +void mdb_init_backends(MdbHandle *mdb) +{ + if (mdb->backends) { + mdb_remove_backends(mdb); + } + mdb->backends = g_hash_table_new(g_str_hash, g_str_equal); + + mdb_register_backend(mdb, "access", + MDB_SHEXP_DROPTABLE|MDB_SHEXP_CST_NOTNULL|MDB_SHEXP_DEFVALUES, + mdb_access_types, NULL, NULL, + "Date()", "Date()", + NULL, + NULL, + "-- That file uses encoding %s\n", + "DROP TABLE %s;\n", + NULL, + NULL, + NULL, + NULL, + NULL, + quote_schema_name_bracket_merge); + mdb_register_backend(mdb, "sybase", + MDB_SHEXP_DROPTABLE|MDB_SHEXP_CST_NOTNULL|MDB_SHEXP_CST_NOTEMPTY|MDB_SHEXP_COMMENTS|MDB_SHEXP_DEFVALUES, + mdb_sybase_types, &mdb_sybase_shortdate_type, NULL, + "getdate()", "getdate()", + NULL, + NULL, + "-- That file uses encoding %s\n", + "DROP TABLE %s;\n", + "ALTER TABLE %s ADD CHECK (%s <>'');\n", + "COMMENT ON COLUMN %s.%s IS %s;\n", + NULL, + "COMMENT ON TABLE %s IS %s;\n", + NULL, + quote_schema_name_dquote); + mdb_register_backend(mdb, "oracle", + MDB_SHEXP_DROPTABLE|MDB_SHEXP_CST_NOTNULL|MDB_SHEXP_COMMENTS|MDB_SHEXP_INDEXES|MDB_SHEXP_RELATIONS|MDB_SHEXP_DEFVALUES, + mdb_oracle_types, &mdb_oracle_shortdate_type, NULL, + "current_date", "sysdate", + NULL, + NULL, + "-- That file uses encoding %s\n", + "DROP TABLE %s;\n", + NULL, + "COMMENT ON COLUMN %s.%s IS %s;\n", + NULL, + "COMMENT ON TABLE %s IS %s;\n", + NULL, + quote_schema_name_dquote); + mdbi_register_backend2(mdb, "postgres", + MDB_SHEXP_DROPTABLE|MDB_SHEXP_CST_NOTNULL|MDB_SHEXP_CST_NOTEMPTY|MDB_SHEXP_COMMENTS|MDB_SHEXP_INDEXES|MDB_SHEXP_RELATIONS|MDB_SHEXP_DEFVALUES|MDB_SHEXP_BULK_INSERT, + mdb_postgres_types, &mdb_postgres_shortdate_type, &mdb_postgres_serial_type, + "current_date", "now()", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d", + "SET client_encoding = '%s';\n", + "CREATE TABLE IF NOT EXISTS %s\n", + "DROP TABLE IF EXISTS %s;\n", + "ALTER TABLE %s ADD CHECK (%s <>'');\n", + "COMMENT ON COLUMN %s.%s IS %s;\n", + NULL, + "COMMENT ON TABLE %s IS %s;\n", + NULL, + quote_schema_name_dquote, + to_lower_case); + mdb_register_backend(mdb, "mysql", + MDB_SHEXP_DROPTABLE|MDB_SHEXP_CST_NOTNULL|MDB_SHEXP_CST_NOTEMPTY|MDB_SHEXP_INDEXES|MDB_SHEXP_RELATIONS|MDB_SHEXP_DEFVALUES|MDB_SHEXP_BULK_INSERT, + mdb_mysql_types, &mdb_mysql_shortdate_type, &mdb_mysql_serial_type, + "current_date", "now()", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d", + "-- That file uses encoding %s\n", + "DROP TABLE IF EXISTS %s;\n", + "ALTER TABLE %s ADD CHECK (%s <>'');\n", + NULL, + "COMMENT %s", + NULL, + "COMMENT %s", + quote_schema_name_rquotes_merge); + mdb_register_backend(mdb, "sqlite", + MDB_SHEXP_DROPTABLE|MDB_SHEXP_DEFVALUES|MDB_SHEXP_BULK_INSERT|MDB_SHEXP_INDEXES|MDB_SHEXP_CST_NOTNULL, + mdb_sqlite_types, NULL, NULL, + "date('now')", "date('now')", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d", + "-- That file uses encoding %s\n", + "DROP TABLE IF EXISTS %s;\n", + NULL, + NULL, + NULL, + NULL, + NULL, + quote_schema_name_rquotes_merge); +} + +MdbBackend *mdbi_register_backend2(MdbHandle *mdb, char *backend_name, guint32 capabilities, + const MdbBackendType *backend_type, const MdbBackendType *type_shortdate, const MdbBackendType *type_autonum, + const char *short_now, const char *long_now, + const char *date_fmt, const char *shortdate_fmt, + const char *charset_statement, + const char *create_table_statement, + const char *drop_statement, + const char *constraint_not_empty_statement, + const char *column_comment_statement, + const char *per_column_comment_statement, + const char *table_comment_statement, + const char *per_table_comment_statement, + gchar* (*quote_schema_name)(const gchar*, const gchar*), + gchar* (*normalise_case)(const gchar*)) { + MdbBackend *backend = g_malloc0(sizeof(MdbBackend)); + backend->capabilities = capabilities; + backend->types_table = backend_type; + backend->type_shortdate = type_shortdate; + backend->type_autonum = type_autonum; + backend->short_now = short_now; + backend->long_now = long_now; + backend->date_fmt = date_fmt; + backend->shortdate_fmt = shortdate_fmt; + backend->charset_statement = charset_statement; + backend->create_table_statement = create_table_statement; + backend->drop_statement = drop_statement; + backend->constaint_not_empty_statement = constraint_not_empty_statement; + backend->column_comment_statement = column_comment_statement; + backend->per_column_comment_statement = per_column_comment_statement; + backend->table_comment_statement = table_comment_statement; + backend->per_table_comment_statement = per_table_comment_statement; + backend->quote_schema_name = quote_schema_name; + backend->normalise_case = normalise_case; + g_hash_table_insert(mdb->backends, backend_name, backend); + return backend; +} + +void mdb_register_backend(MdbHandle *mdb, char *backend_name, guint32 capabilities, + const MdbBackendType *backend_type, const MdbBackendType *type_shortdate, const MdbBackendType *type_autonum, + const char *short_now, const char *long_now, + const char *date_fmt, const char *shortdate_fmt, + const char *charset_statement, + const char *drop_statement, + const char *constraint_not_empty_statement, + const char *column_comment_statement, + const char *per_column_comment_statement, + const char *table_comment_statement, + const char *per_table_comment_statement, + gchar* (*quote_schema_name)(const gchar*, const gchar*)) +{ + mdbi_register_backend2(mdb, backend_name, capabilities, + backend_type, type_shortdate, type_autonum, + short_now, long_now, + date_fmt, shortdate_fmt, + charset_statement, + "CREATE TABLE %s\n", + drop_statement, + constraint_not_empty_statement, + column_comment_statement, + per_column_comment_statement, + table_comment_statement, + per_table_comment_statement, + quote_schema_name, + passthrough_unchanged); +} + +/** + * mdb_remove_backends + * + * Removes all entries from and destroys the mdb_backends hash. + */ +void +mdb_remove_backends(MdbHandle *mdb) +{ + g_hash_table_foreach(mdb->backends, mdb_drop_backend, NULL); + g_hash_table_destroy(mdb->backends); +} +static void mdb_drop_backend(gpointer key, gpointer value, gpointer data) +{ + MdbBackend *backend = (MdbBackend *)value; + g_free (backend); +} + +/** + * mdb_set_default_backend + * @mdb: Handle to open MDB database file + * @backend_name: Name of the backend to set as default + * + * Sets the default backend of the handle @mdb to @backend_name. + * + * Returns: 1 if successful, 0 if unsuccessful. + */ +int mdb_set_default_backend(MdbHandle *mdb, const char *backend_name) +{ + MdbBackend *backend; + + if (!mdb->backends) { + mdb_init_backends(mdb); + } + backend = (MdbBackend *) g_hash_table_lookup(mdb->backends, backend_name); + if (backend) { + mdb->default_backend = backend; + g_free(mdb->backend_name); // NULL is ok + mdb->backend_name = (char *) g_strdup(backend_name); + mdb->relationships_table = NULL; + if (backend->date_fmt) { + mdb_set_date_fmt(mdb, backend->date_fmt); + } else { + mdb_set_date_fmt(mdb, "%x %X"); + } + if (backend->shortdate_fmt) { + mdb_set_shortdate_fmt(mdb, backend->shortdate_fmt); + } else { + mdb_set_shortdate_fmt(mdb, "%x"); + } + } + return (backend != NULL); +} + + +/** + * Generates index name based on backend. + * + * You should free() the returned value once you are done with it. + * + * @param backend backend we are generating indexes for + * @param table table being processed + * @param idx index being processed + * @return the index name + */ +static char * +mdb_get_index_name(int backend, MdbTableDef *table, MdbIndex *idx) +{ + char *index_name; + + switch(backend){ + case MDB_BACKEND_MYSQL: + // appending table name to index often makes it too long for mysql + if (idx->index_type==1) + // for mysql name of primary key is not used + index_name = g_strdup("_pkey"); + else { + index_name = g_strdup(idx->name); + } + break; + default: + if (idx->index_type==1) + index_name = g_strconcat(table->name, "_pkey", NULL); + else { + index_name = g_strconcat(table->name, "_", idx->name, "_idx", NULL); + } + } + + return index_name; +} +/** + * mdb_print_pk - print primary key constraint + * @output: Where to print the sql + * @table: Table to process + */ +static void +mdb_print_pk_if_sqlite(FILE *outfile, MdbTableDef *table) +{ + unsigned int i, j; + MdbHandle *mdb = table->entry->mdb; + MdbIndex *idx; + MdbColumn *col; + char *quoted_name; + // this is only necessary for sqlite + if (strcmp(mdb->backend_name, "sqlite") != 0) + return; + + if (table->indices==NULL) + mdb_read_indices(table); + + for (i = 0; i < table->num_idxs; i++) { + idx = g_ptr_array_index(table->indices, i); + if (idx->index_type == 1) { + fprintf(outfile, "\t, PRIMARY KEY ("); + for (j = 0; j < idx->num_keys; j++) { + if (j) + fprintf(outfile, ", "); + col = g_ptr_array_index(table->columns, idx->key_col_num[j] - 1); + quoted_name = mdb->default_backend->quote_schema_name(NULL, col->name); + quoted_name = mdb_normalise_and_replace(mdb, "ed_name); + fprintf(outfile, "%s", quoted_name); + if (idx->index_type != 1 && idx->key_col_order[j]) + /* no DESC for primary keys */ + fprintf(outfile, " DESC"); + + g_free(quoted_name); + } + fprintf(outfile, ")\n"); + } + } +} +/** + * mdb_print_indexes + * @output: Where to print the sql + * @table: Table to process + * @dbnamespace: Target namespace/schema name + */ +static void +mdb_print_indexes(FILE* outfile, MdbTableDef *table, char *dbnamespace) +{ + unsigned int i, j; + char* quoted_table_name; + char* index_name; + char* quoted_name; + int backend; + MdbHandle* mdb = table->entry->mdb; + MdbIndex *idx; + MdbColumn *col; + + if (!strcmp(mdb->backend_name, "postgres")) { + backend = MDB_BACKEND_POSTGRES; + } else if (!strcmp(mdb->backend_name, "mysql")) { + backend = MDB_BACKEND_MYSQL; + } else if (!strcmp(mdb->backend_name, "oracle")) { + backend = MDB_BACKEND_ORACLE; + } else if (!strcmp(mdb->backend_name, "sqlite")) { + backend = MDB_BACKEND_SQLITE; + } else { + fprintf(outfile, "-- Indexes are not implemented for %s\n\n", mdb->backend_name); + return; + } + + /* read indexes */ + if (table->indices==NULL) + mdb_read_indices(table); + + fprintf (outfile, "-- CREATE INDEXES ...\n"); + + quoted_table_name = mdb->default_backend->quote_schema_name(dbnamespace, table->name); + quoted_table_name = mdb->default_backend->normalise_case(quoted_table_name); + + for (i=0;inum_idxs;i++) { + idx = g_ptr_array_index (table->indices, i); + if (idx->index_type==2) + continue; + /* Sqlite3 primary keys have to be issued as a table constraint */ + if (idx->index_type == 1 && backend == MDB_BACKEND_SQLITE) + continue; + + index_name = mdb_get_index_name(backend, table, idx); + switch (backend) { + case MDB_BACKEND_POSTGRES: + /* PostgreSQL index and constraint names are + * never namespaced in DDL (they are always + * created in same namespace as table), so + * omit namespace. + */ + quoted_name = mdb->default_backend->quote_schema_name(NULL, index_name); + break; + + default: + quoted_name = mdb->default_backend->quote_schema_name(dbnamespace, index_name); + } + + quoted_name = mdb_normalise_and_replace(mdb, "ed_name); + if (idx->num_keys == 0) { + fprintf(outfile, "-- WARNING: found no keys for index %s - ignored\n", quoted_name); + continue; + } + if (idx->index_type==1) { + switch (backend) { + case MDB_BACKEND_ORACLE: + case MDB_BACKEND_POSTGRES: + fprintf (outfile, "ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (", quoted_table_name, quoted_name); + break; + case MDB_BACKEND_MYSQL: + fprintf (outfile, "ALTER TABLE %s ADD PRIMARY KEY (", quoted_table_name); + break; + } + } else { + switch (backend) { + case MDB_BACKEND_ORACLE: + case MDB_BACKEND_POSTGRES: + case MDB_BACKEND_SQLITE: + fprintf(outfile, "CREATE"); + if (idx->flags & MDB_IDX_UNIQUE) + fprintf (outfile, " UNIQUE"); + fprintf(outfile, " INDEX %s ON %s (", quoted_name, quoted_table_name); + break; + case MDB_BACKEND_MYSQL: + fprintf(outfile, "ALTER TABLE %s ADD", quoted_table_name); + if (idx->flags & MDB_IDX_UNIQUE) + fprintf (outfile, " UNIQUE"); + fprintf(outfile, " INDEX %s (", quoted_name); + break; + } + } + g_free(quoted_name); + free(index_name); + + for (j=0;jnum_keys;j++) { + if (j) + fprintf(outfile, ", "); + col=g_ptr_array_index(table->columns,idx->key_col_num[j]-1); + quoted_name = mdb->default_backend->quote_schema_name(NULL, col->name); + quoted_name = mdb_normalise_and_replace(mdb, "ed_name); + fprintf (outfile, "%s", quoted_name); + if (idx->index_type!=1 && idx->key_col_order[j]) + /* no DESC for primary keys */ + fprintf(outfile, " DESC"); + + g_free(quoted_name); + + } + fprintf (outfile, ");\n"); + } + fputc ('\n', outfile); + + g_free(quoted_table_name); +} + +/** + * mdb_get_relationships + * @mdb: Handle to open MDB database file + * @tablename: Name of the table to process. Process all tables if NULL. + * + * Generates relationships by reading the MSysRelationships table. + * 'szColumn' contains the column name of the child table. + * 'szObject' contains the table name of the child table. + * 'szReferencedColumn' contains the column name of the parent table. + * 'szReferencedObject' contains the table name of the parent table. + * 'grbit' contains integrity constraints. + * + * Returns: a string stating that relationships are not supported for the + * selected backend, or a string containing SQL commands for setting up + * the relationship, tailored for the selected backend. + * Returns NULL on last iteration. + * The caller is responsible for freeing this string. + */ +static char * +mdb_get_relationships(MdbHandle *mdb, const gchar *dbnamespace, const char* tablename) +{ + unsigned int i; + gchar *text = NULL; /* String to be returned */ + char **bound = mdb->relationships_values; /* Bound values */ + int backend = 0; + char *quoted_table_1, *quoted_column_1, + *quoted_table_2, *quoted_column_2, + *constraint_name, *quoted_constraint_name; + long grbit; + + if (!strcmp(mdb->backend_name, "oracle")) { + backend = MDB_BACKEND_ORACLE; + } else if (!strcmp(mdb->backend_name, "postgres")) { + backend = MDB_BACKEND_POSTGRES; + } else if (!strcmp(mdb->backend_name, "mysql")) { + backend = MDB_BACKEND_MYSQL; + } else if (!mdb->relationships_table) { + return NULL; + } + + if (!mdb->relationships_table) { + mdb->relationships_table = mdb_read_table_by_name(mdb, "MSysRelationships", MDB_TABLE); + if (!mdb->relationships_table || !mdb->relationships_table->num_rows) { + fprintf(stderr, "No MSysRelationships\n"); + return NULL; + } + if (!mdb_read_columns(mdb->relationships_table)) { + fprintf(stderr, "Unable to read columns of MSysRelationships\n"); + return NULL; + } + for (i=0;i<5;i++) { + bound[i] = g_malloc0(mdb->bind_size); + } + mdb_bind_column_by_name(mdb->relationships_table, "szColumn", bound[0], NULL); + mdb_bind_column_by_name(mdb->relationships_table, "szObject", bound[1], NULL); + mdb_bind_column_by_name(mdb->relationships_table, "szReferencedColumn", bound[2], NULL); + mdb_bind_column_by_name(mdb->relationships_table, "szReferencedObject", bound[3], NULL); + mdb_bind_column_by_name(mdb->relationships_table, "grbit", bound[4], NULL); + mdb_rewind_table(mdb->relationships_table); + } + if (mdb->relationships_table->cur_row >= mdb->relationships_table->num_rows) { /* past the last row */ + for (i=0;i<5;i++) + g_free(bound[i]); + mdb->relationships_table = NULL; + return NULL; + } + + while (1) { + if (!mdb_fetch_row(mdb->relationships_table)) { + for (i=0;i<5;i++) + g_free(bound[i]); + mdb->relationships_table = NULL; + return NULL; + } + if (!tablename || !strcmp(bound[1], tablename)) + break; + } + + quoted_table_1 = mdb->default_backend->quote_schema_name(dbnamespace, bound[1]); + quoted_table_2 = mdb->default_backend->quote_schema_name(dbnamespace, bound[3]); + grbit = atoi(bound[4]); + constraint_name = g_strconcat(bound[1], "_", bound[0], "_fk", NULL); + + switch (backend) { + case MDB_BACKEND_POSTGRES: + /* PostgreSQL index and constraint names are + * never namespaced in DDL (they are always + * created in same namespace as table), so + * omit namespace. Nor should column names + * be namespaced. + */ + quoted_constraint_name = mdb->default_backend->quote_schema_name(NULL, constraint_name); + quoted_constraint_name = mdb_normalise_and_replace(mdb, "ed_constraint_name); + quoted_column_1 = mdb->default_backend->quote_schema_name(NULL, bound[0]); + quoted_column_1 = mdb_normalise_and_replace(mdb, "ed_column_1); + quoted_column_2 = mdb->default_backend->quote_schema_name(NULL, bound[2]); + quoted_column_2 = mdb_normalise_and_replace(mdb, "ed_column_2); + break; + + default: + /* Other databases, namespace constraint and + * column names. + */ + quoted_constraint_name = mdb->default_backend->quote_schema_name(dbnamespace, constraint_name); + quoted_column_1 = mdb->default_backend->quote_schema_name(dbnamespace, bound[0]); + quoted_column_2 = mdb->default_backend->quote_schema_name(dbnamespace, bound[2]); + break; + } + g_free(constraint_name); + + if (grbit & 0x00000002) { + text = g_strconcat( + "-- Relationship from ", quoted_table_1, + " (", quoted_column_1, ")" + " to ", quoted_table_2, "(", quoted_column_2, ")", + " does not enforce integrity.\n", NULL); + } else { + switch (backend) { + case MDB_BACKEND_ORACLE: + text = g_strconcat( + "ALTER TABLE ", quoted_table_1, + " ADD CONSTRAINT ", quoted_constraint_name, + " FOREIGN KEY (", quoted_column_1, ")" + " REFERENCES ", quoted_table_2, "(", quoted_column_2, ")", + (grbit & 0x00001000) ? " ON DELETE CASCADE" : "", + ";\n", NULL); + + break; + case MDB_BACKEND_MYSQL: + text = g_strconcat( + "ALTER TABLE ", quoted_table_1, + " ADD CONSTRAINT ", quoted_constraint_name, + " FOREIGN KEY (", quoted_column_1, ")" + " REFERENCES ", quoted_table_2, "(", quoted_column_2, ")", + (grbit & 0x00000100) ? " ON UPDATE CASCADE" : "", + (grbit & 0x00001000) ? " ON DELETE CASCADE" : "", + ";\n", NULL); + break; + case MDB_BACKEND_POSTGRES: + text = g_strconcat( + "ALTER TABLE ", quoted_table_1, + " ADD CONSTRAINT ", quoted_constraint_name, + " FOREIGN KEY (", quoted_column_1, ")" + " REFERENCES ", quoted_table_2, "(", quoted_column_2, ")", + (grbit & 0x00000100) ? " ON UPDATE CASCADE" : "", + (grbit & 0x00001000) ? " ON DELETE CASCADE" : "", + /* On some databases (eg PostgreSQL) we also want to set + * the constraints to be optionally deferrable, to + * facilitate out of order bulk loading. + */ + " DEFERRABLE", + " INITIALLY IMMEDIATE", + ";\n", NULL); + + break; + } + } + g_free(quoted_table_1); + g_free(quoted_column_1); + g_free(quoted_table_2); + g_free(quoted_column_2); + g_free(quoted_constraint_name); + + return (char *)text; +} + +static void +generate_table_schema(FILE *outfile, MdbCatalogEntry *entry, char *dbnamespace, guint32 export_options) +{ + MdbTableDef *table; + MdbHandle *mdb = entry->mdb; + MdbColumn *col; + unsigned int i; + char* quoted_table_name; + char* quoted_name; + MdbProperties *props; + const char *prop_value; + + quoted_table_name = mdb->default_backend->quote_schema_name(dbnamespace, entry->object_name); + quoted_table_name = mdb_normalise_and_replace(mdb, "ed_table_name); + + /* drop the table if it exists */ + if (export_options & MDB_SHEXP_DROPTABLE) + fprintf (outfile, mdb->default_backend->drop_statement, quoted_table_name); + + /* create the table */ + fprintf (outfile, mdb->default_backend->create_table_statement, quoted_table_name); + fprintf (outfile, " (\n"); + + table = mdb_read_table (entry); + if (!table) { + fprintf(stderr, "Error: Table %s does not exist\n", entry->object_name); + return; + } + + /* get the columns */ + mdb_read_columns(table); + + /* loop over the columns, dumping the names and types */ + for (i = 0; i < table->num_cols; i++) { + col = g_ptr_array_index (table->columns, i); + + quoted_name = mdb->default_backend->quote_schema_name(NULL, col->name); + quoted_name = mdb_normalise_and_replace(mdb, "ed_name); + fprintf (outfile, "\t%s\t\t\t%s", quoted_name, + mdb_get_colbacktype_string (col)); + g_free(quoted_name); + + if (mdb_colbacktype_takes_length(col)) { + /* more portable version from DW patch */ + if (col->col_size == 0) + fputs(" (255)", outfile); + else if (col->col_scale != 0) + fprintf(outfile, " (%d, %d)", col->col_scale, col->col_prec); + else if (!IS_JET3(mdb) && mdb_colbacktype_takes_length_in_characters(col)) + fprintf(outfile, " (%d)", col->col_size/2); + else + fprintf(outfile, " (%d)", col->col_size); + } + + if (mdb->default_backend->per_column_comment_statement && export_options & MDB_SHEXP_COMMENTS) { + prop_value = mdb_col_get_prop(col, "Description"); + if (prop_value) { + char *comment = quote_with_squotes(prop_value); + fputs(" ", outfile); + fprintf(outfile, + mdb->default_backend->per_column_comment_statement, + comment); + free(comment); + } + } + + if (export_options & MDB_SHEXP_CST_NOTNULL) { + if (col->col_type == MDB_BOOL) { + /* access booleans are never null */ + fputs(" NOT NULL", outfile); + } else { + const gchar *not_null = mdb_col_get_prop(col, "Required"); + if (not_null && not_null[0]=='y') + fputs(" NOT NULL", outfile); + } + } + + if (export_options & MDB_SHEXP_DEFVALUES) { + int done = 0; + if (col->props) { + gchar *defval = g_hash_table_lookup(col->props->hash, "DefaultValue"); + if (defval) { + size_t def_len = strlen(defval); + fputs(" DEFAULT ", outfile); + /* ugly hack to detect the type */ + if (defval[0]=='"' && defval[def_len-1]=='"') { + /* this is a string */ + gchar *output_default = malloc(def_len-1); + gchar *output_default_escaped; + memcpy(output_default, defval+1, def_len-2); + output_default[def_len-2] = 0; + output_default_escaped = quote_with_squotes(output_default); + fputs(output_default_escaped, outfile); + g_free(output_default_escaped); + free(output_default); + } else if (!strcmp(defval, "Yes")) + fputs("TRUE", outfile); + else if (!strcmp(defval, "No")) + fputs("FALSE", outfile); + else if (!g_ascii_strcasecmp(defval, "date()")) { + if (mdb_col_is_shortdate(col)) + fputs(mdb->default_backend->short_now, outfile); + else + fputs(mdb->default_backend->long_now, outfile); + } + else + fputs(defval, outfile); + done = 1; + } + } + if (!done && col->col_type == MDB_BOOL) + /* access booleans are false by default */ + fputs(" DEFAULT FALSE", outfile); + } + if (i < table->num_cols - 1) + fputs(", \n", outfile); + else + fputs("\n", outfile); + } /* for */ + + if (export_options & MDB_SHEXP_INDEXES) { + // sqlite does not support ALTER TABLE PRIMARY KEY, so we need to place it directly into CREATE TABLE + mdb_print_pk_if_sqlite(outfile, table); + } + + fputs(")", outfile); + if (mdb->default_backend->per_table_comment_statement && export_options & MDB_SHEXP_COMMENTS) { + prop_value = mdb_table_get_prop(table, "Description"); + if (prop_value) { + char *comment = quote_with_squotes(prop_value); + fputs(" ", outfile); + fprintf(outfile, mdb->default_backend->per_table_comment_statement, comment); + free(comment); + } + } + fputs(";\n", outfile); + + /* Add the constraints on columns */ + for (i = 0; i < table->num_cols; i++) { + col = g_ptr_array_index (table->columns, i); + props = col->props; + if (!props) + continue; + + quoted_name = mdb->default_backend->quote_schema_name(NULL, col->name); + quoted_name = mdb_normalise_and_replace(mdb, "ed_name); + + if (export_options & MDB_SHEXP_CST_NOTEMPTY) { + prop_value = mdb_col_get_prop(col, "AllowZeroLength"); + if (prop_value && prop_value[0]=='n') + fprintf(outfile, + mdb->default_backend->constaint_not_empty_statement, + quoted_table_name, quoted_name); + } + + if (mdb->default_backend->column_comment_statement && export_options & MDB_SHEXP_COMMENTS) { + prop_value = mdb_col_get_prop(col, "Description"); + if (prop_value) { + char *comment = quote_with_squotes(prop_value); + fprintf(outfile, + mdb->default_backend->column_comment_statement, + quoted_table_name, quoted_name, comment); + g_free(comment); + } + } + + g_free(quoted_name); + } + + /* Add the constraints on table */ + if (mdb->default_backend->table_comment_statement && export_options & MDB_SHEXP_COMMENTS) { + prop_value = mdb_table_get_prop(table, "Description"); + if (prop_value) { + char *comment = quote_with_squotes(prop_value); + fprintf(outfile, + mdb->default_backend->table_comment_statement, + quoted_table_name, comment); + g_free(comment); + } + } + fputc('\n', outfile); + + + if (export_options & MDB_SHEXP_INDEXES) + // prints all the indexes of that table + mdb_print_indexes(outfile, table, dbnamespace); + + g_free(quoted_table_name); + + mdb_free_tabledef (table); +} + + +int +mdb_print_schema(MdbHandle *mdb, FILE *outfile, char *tabname, char *dbnamespace, guint32 export_options) +{ + unsigned int i; + char *the_relation; + MdbCatalogEntry *entry; + const char *charset; + int success = (tabname == NULL); + + /* clear unsupported options */ + export_options &= mdb->default_backend->capabilities; + + /* Print out a little message to show that this came from mdb-tools. + I like to know how something is generated. DW */ + fputs("-- ----------------------------------------------------------\n" + "-- MDB Tools - A library for reading MS Access database files\n" + "-- Copyright (C) 2000-2011 Brian Bruns and others.\n" + "-- Files in libmdb are licensed under LGPL and the utilities under\n" + "-- the GPL, see COPYING.LIB and COPYING files respectively.\n" + "-- Check out http://mdbtools.sourceforge.net\n" + "-- ----------------------------------------------------------\n\n", + outfile); + + charset = mdb_target_charset(mdb); + if (charset) { + fprintf(outfile, mdb->default_backend->charset_statement, charset); + fputc('\n', outfile); + } + + for (i=0; i < mdb->num_catalog; i++) { + entry = g_ptr_array_index (mdb->catalog, i); + if (entry->object_type == MDB_TABLE) { + if ((tabname && !strcmp(entry->object_name, tabname)) + || (!tabname && mdb_is_user_table(entry))) { + generate_table_schema(outfile, entry, dbnamespace, export_options); + success = 1; + } + } + } + fprintf (outfile, "\n"); + + if (export_options & MDB_SHEXP_RELATIONS) { + fputs ("-- CREATE Relationships ...\n", outfile); + the_relation=mdb_get_relationships(mdb, dbnamespace, tabname); + if (!the_relation) { + fputs("-- relationships are not implemented for ", outfile); + fputs(mdb->backend_name, outfile); + fputs("\n", outfile); + } else { + do { + fputs(the_relation, outfile); + g_free(the_relation); + } while ((the_relation=mdb_get_relationships(mdb, dbnamespace, tabname)) != NULL); + } + } + return success; +} + +#define MDB_BINEXPORT_MASK 0x0F +#define is_binary_type(x) (x==MDB_OLE || x==MDB_BINARY || x==MDB_REPID) +#define is_quote_type(x) (is_binary_type(x) || x==MDB_TEXT || x==MDB_MEMO || x==MDB_DATETIME) +//#define DONT_ESCAPE_ESCAPE +void +mdb_print_col(FILE *outfile, gchar *col_val, int quote_text, int col_type, int bin_len, + char *quote_char, char *escape_char, int flags) +/* quote_text: Don't quote if 0. + */ +{ + size_t quote_len = strlen(quote_char); /* multibyte */ + + size_t orig_escape_len = escape_char ? strlen(escape_char) : 0; + int quoting = quote_text && is_quote_type(col_type); + int bin_mode = (flags & MDB_BINEXPORT_MASK); + int escape_cr_lf = !!(flags & MDB_EXPORT_ESCAPE_CONTROL_CHARS); + + /* double the quote char if no escape char passed */ + if (!escape_char) + escape_char = quote_char; + + if (quoting) + fputs(quote_char, outfile); + + while (1) { + if (is_binary_type(col_type)) { + if (bin_mode == MDB_BINEXPORT_STRIP) + break; + if (!bin_len--) + break; + } else /* use \0 sentry */ + if (!*col_val) + break; + + if (is_binary_type(col_type) && bin_mode == MDB_BINEXPORT_OCTAL) { + fprintf(outfile, "\\%03o", *(unsigned char*)col_val++); + } else if (is_binary_type(col_type) && bin_mode == MDB_BINEXPORT_HEXADECIMAL) { + fprintf(outfile, "%02X", *(unsigned char*)col_val++); + } else if (quoting && quote_len && !strncmp(col_val, quote_char, quote_len)) { + fprintf(outfile, "%s%s", escape_char, quote_char); + col_val += quote_len; +#ifndef DONT_ESCAPE_ESCAPE + } else if (quoting && orig_escape_len && !strncmp(col_val, escape_char, orig_escape_len)) { + fprintf(outfile, "%s%s", escape_char, escape_char); + col_val += orig_escape_len; +#endif + } else if (escape_cr_lf && is_quote_type(col_type) && *col_val=='\r') { + col_val++; + putc('\\', outfile); + putc('r', outfile); + } else if (escape_cr_lf && is_quote_type(col_type) && *col_val=='\n') { + col_val++; + putc('\\', outfile); + putc('n', outfile); + } else if (escape_cr_lf && is_quote_type(col_type) && *col_val=='\t') { + col_val++; + putc('\\', outfile); + putc('t', outfile); + } else if (escape_cr_lf && is_quote_type(col_type) && *col_val=='\\') { + col_val++; + putc('\\', outfile); + putc('\\', outfile); + } else + putc(*col_val++, outfile); + } + if (quoting) + fputs(quote_char, outfile); +} diff --git a/wlx/dbview/src/libmdb/catalog.c b/wlx/dbview/src/libmdb/catalog.c new file mode 100644 index 0000000..9239be2 --- /dev/null +++ b/wlx/dbview/src/libmdb/catalog.c @@ -0,0 +1,203 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mdbtools.h" + +const char * +mdb_get_objtype_string(int obj_type) +{ + static const char *type_name[] = {"Form", + "Table", + "Macro", + "System Table", + "Report", + "Query", + "Linked Table", + "Module", + "Relationship", + "Unknown 0x09", + "User Info", + "Database" + }; + + if (obj_type >= (int)(sizeof(type_name)/sizeof(type_name[0]))) { + return NULL; + } else { + return type_name[obj_type]; + } +} + +void mdb_free_catalog(MdbHandle *mdb) +{ + guint i, j; + MdbCatalogEntry *entry; + + if ((!mdb) || (!mdb->catalog)) return; + for (i=0; icatalog->len; i++) { + entry = (MdbCatalogEntry *)g_ptr_array_index(mdb->catalog, i); + if (entry) { + if (entry->props) { + for (j=0; jprops->len; j++) + mdb_free_props(g_ptr_array_index(entry->props, j)); + g_ptr_array_free(entry->props, TRUE); + } + g_free(entry); + } + } + g_ptr_array_free(mdb->catalog, TRUE); + mdb->catalog = NULL; +} + +GPtrArray *mdb_read_catalog (MdbHandle *mdb, int objtype) +{ + MdbCatalogEntry *entry, msysobj; + MdbTableDef *table; + char *obj_id = NULL; + char *obj_name = NULL; + char *obj_type = NULL; + char *obj_flags = NULL; + char *obj_props = NULL; + int type; + int i; + MdbColumn *col_props; + int kkd_size_ole; + + if (!mdb) return NULL; + if (mdb->catalog) mdb_free_catalog(mdb); + mdb->catalog = g_ptr_array_new(); + mdb->num_catalog = 0; + + obj_id = malloc(mdb->bind_size); + obj_name = malloc(mdb->bind_size); + obj_type = malloc(mdb->bind_size); + obj_flags = malloc(mdb->bind_size); + obj_props = malloc(mdb->bind_size); + + /* dummy up a catalog entry so we may read the table def */ + memset(&msysobj, 0, sizeof(MdbCatalogEntry)); + msysobj.mdb = mdb; + msysobj.object_type = MDB_TABLE; + msysobj.table_pg = 2; + snprintf(msysobj.object_name, sizeof(msysobj.object_name), "%s", "MSysObjects"); + + /* mdb_table_dump(&msysobj); */ + + table = mdb_read_table(&msysobj); + if (!table) { + fprintf(stderr, "Unable to read table %s\n", msysobj.object_name); + mdb_free_catalog(mdb); + goto cleanup; + } + + if (!mdb_read_columns(table)) { + fprintf(stderr, "Unable to read columns of table %s\n", msysobj.object_name); + mdb_free_catalog(mdb); + goto cleanup; + } + + if (mdb_bind_column_by_name(table, "Id", obj_id, NULL) == -1 || + mdb_bind_column_by_name(table, "Name", obj_name, NULL) == -1 || + mdb_bind_column_by_name(table, "Type", obj_type, NULL) == -1 || + mdb_bind_column_by_name(table, "Flags", obj_flags, NULL) == -1) { + fprintf(stderr, "Unable to bind columns from table %s (%d columns found)\n", + msysobj.object_name, table->num_cols); + mdb_free_catalog(mdb); + goto cleanup; + } + if ((i = mdb_bind_column_by_name(table, "LvProp", obj_props, &kkd_size_ole)) == -1) { + fprintf(stderr, "Unable to bind column %s from table %s\n", "LvProp", msysobj.object_name); + mdb_free_catalog(mdb); + goto cleanup; + } + col_props = g_ptr_array_index(table->columns, i-1); + + mdb_rewind_table(table); + + while (mdb_fetch_row(table)) { + type = atoi(obj_type); + if (objtype==MDB_ANY || type == objtype) { + //fprintf(stderr, "obj_id: %10ld objtype: %-3d (0x%04x) obj_name: %s\n", + // (atol(obj_id) & 0x00FFFFFF), type, type, obj_name); + entry = g_malloc0(sizeof(MdbCatalogEntry)); + entry->mdb = mdb; + snprintf(entry->object_name, sizeof(entry->object_name), "%s", obj_name); + entry->object_type = (type & 0x7F); + entry->table_pg = atol(obj_id) & 0x00FFFFFF; + entry->flags = atol(obj_flags); + mdb->num_catalog++; + g_ptr_array_add(mdb->catalog, entry); + if (kkd_size_ole) { + size_t kkd_len; + void *kkd = mdb_ole_read_full(mdb, col_props, &kkd_len); + //mdb_buffer_dump(kkd, 0, kkd_len); + if (kkd) { + entry->props = mdb_kkd_to_props(mdb, kkd, kkd_len); + free(kkd); + } + } + } + } + //mdb_dump_catalog(mdb, MDB_TABLE); + +cleanup: + if (table) + mdb_free_tabledef(table); + + free(obj_id); + free(obj_name); + free(obj_type); + free(obj_flags); + free(obj_props); + + return mdb->catalog; +} + + +MdbCatalogEntry * +mdb_get_catalogentry_by_name(MdbHandle *mdb, const gchar* name) +{ + unsigned int i; + MdbCatalogEntry *entry; + + for (i=0; inum_catalog; i++) { + entry = g_ptr_array_index(mdb->catalog, i); + if (!g_ascii_strcasecmp(entry->object_name, name)) + return entry; + } + return NULL; +} + +void +mdb_dump_catalog(MdbHandle *mdb, int obj_type) +{ + unsigned int i; + MdbCatalogEntry *entry; + + mdb_read_catalog(mdb, obj_type); + for (i=0;inum_catalog;i++) { + entry = g_ptr_array_index(mdb->catalog,i); + if (obj_type==MDB_ANY || entry->object_type==obj_type) { + printf("Type: %-12s Name: %-48s Page: %06lx\n", + mdb_get_objtype_string(entry->object_type) ?: "Unknown", + entry->object_name, + entry->table_pg); + } + } + return; +} + diff --git a/wlx/dbview/src/libmdb/data.c b/wlx/dbview/src/libmdb/data.c new file mode 100644 index 0000000..5b5668b --- /dev/null +++ b/wlx/dbview/src/libmdb/data.c @@ -0,0 +1,1146 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mdbtools.h" + +#include + +#define OFFSET_MASK 0x1fff +#define OLE_BUFFER_SIZE (MDB_BIND_SIZE*64) + +static int _mdb_attempt_bind(MdbHandle *mdb, + MdbColumn *col, unsigned char isnull, int offset, int len); +static char *mdb_date_to_string(MdbHandle *mdb, const char *fmt, void *buf, int start); +#ifdef MDB_COPY_OLE +static size_t mdb_copy_ole(MdbHandle *mdb, void *dest, int start, int size); +#endif + +#ifndef HAVE_REALLOCF +static void *reallocf(void *ptr, size_t len) { + void *ptr2 = realloc(ptr, len); + if (!ptr2) { + free(ptr); + return NULL; + } + return ptr2; +} +#endif + +static const int noleap_cal[] = {0,31,59,90,120,151,181,212,243,273,304,334,365}; +static const int leap_cal[] = {0,31,60,91,121,152,182,213,244,274,305,335,366}; + +/* Some databases (eg PostgreSQL) do not understand integer 0/1 values + * as TRUE/FALSE, so provide a means to override the values used to be + * the SQL Standard TRUE/FALSE values. + */ +static const char boolean_false_number[] = "0"; +static const char boolean_true_number[] = "1"; + +static const char boolean_false_word[] = "FALSE"; +static const char boolean_true_word[] = "TRUE"; + +void mdb_set_bind_size(MdbHandle *mdb, size_t bind_size) { + mdb->bind_size = bind_size; +} + +void mdb_set_date_fmt(MdbHandle *mdb, const char *fmt) +{ + snprintf(mdb->date_fmt, sizeof(mdb->date_fmt), "%s", fmt); +} + +void mdb_set_shortdate_fmt(MdbHandle *mdb, const char *fmt) +{ + snprintf(mdb->shortdate_fmt, sizeof(mdb->shortdate_fmt), "%s", fmt); +} + +void mdb_set_repid_fmt(MdbHandle *mdb, MdbUuidFormat format) +{ + mdb->repid_fmt = format; +} + +void mdb_set_boolean_fmt_numbers(MdbHandle *mdb) +{ + mdb->boolean_false_value = boolean_false_number; + mdb->boolean_true_value = boolean_true_number; +} + +void mdb_set_boolean_fmt_words(MdbHandle *mdb) +{ + mdb->boolean_false_value = boolean_false_word; + mdb->boolean_true_value = boolean_true_word; +} + +int mdb_bind_column(MdbTableDef *table, int col_num, void *bind_ptr, int *len_ptr) +{ + MdbColumn *col = NULL; + + if (!table->columns) + return -1; + /* + ** the column arrary is 0 based, so decrement to get 1 based parameter + */ + col_num--; + + if (col_num >= 0 && col_num < (int)table->num_cols) { + col=g_ptr_array_index(table->columns, col_num); + + if (col) { + if (bind_ptr) + col->bind_ptr = bind_ptr; + if (len_ptr) + col->len_ptr = len_ptr; + + return col_num + 1; + } + } + + return -1; +} + +int +mdb_bind_column_by_name(MdbTableDef *table, gchar *col_name, void *bind_ptr, int *len_ptr) +{ + unsigned int i; + int col_num = -1; + MdbColumn *col; + + if (!table->columns) + return -1; + + for (i=0;inum_cols;i++) { + col=g_ptr_array_index(table->columns,i); + if (!g_ascii_strcasecmp(col->name,col_name)) { + col_num = i + 1; + if (bind_ptr) + col->bind_ptr = bind_ptr; + if (len_ptr) + col->len_ptr = len_ptr; + break; + } + } + + return col_num; +} + +/** + * mdb_find_pg_row + * @mdb: Database file handle + * @pg_row: Lower byte contains the row number, the upper three contain page + * @buf: Pointer for returning a pointer to the page + * @off: Pointer for returning an offset to the row + * @len: Pointer for returning the length of the row + * + * Returns: 0 on success. -1 on failure. + */ +int mdb_find_pg_row(MdbHandle *mdb, int pg_row, void **buf, int *off, size_t *len) +{ + unsigned int pg = pg_row >> 8; + unsigned int row = pg_row & 0xff; + int result = 0; + + if (mdb_read_alt_pg(mdb, pg) != mdb->fmt->pg_size) + return -1; + mdb_swap_pgbuf(mdb); + result = mdb_find_row(mdb, row, off, len); + mdb_swap_pgbuf(mdb); + *off &= OFFSET_MASK; + *buf = mdb->alt_pg_buf; + return result; +} + +int mdb_find_row(MdbHandle *mdb, int row, int *start, size_t *len) +{ + int rco = mdb->fmt->row_count_offset; + int next_start; + + if (row > 1000) return -1; + + *start = mdb_get_int16(mdb->pg_buf, rco + 2 + row*2); + next_start = (row == 0) ? mdb->fmt->pg_size : + mdb_get_int16(mdb->pg_buf, rco + row*2) & OFFSET_MASK; + *len = next_start - (*start & OFFSET_MASK); + + if ((*start & OFFSET_MASK) >= mdb->fmt->pg_size || + (*start & OFFSET_MASK) > next_start || + next_start > mdb->fmt->pg_size) + return -1; + + return 0; +} + +int +mdb_find_end_of_row(MdbHandle *mdb, int row) +{ + int rco = mdb->fmt->row_count_offset; + int row_end; + +#if 1 + if (row > 1000) return -1; + + row_end = (row == 0) ? mdb->fmt->pg_size : + mdb_get_int16(mdb->pg_buf, rco + row*2) & OFFSET_MASK; +#else + /* Search the previous "row start" values for the first non-'lookupflag' + * one. If we don't find one, then the end of the page is the correct + * value. + */ + int i, row_start; + + if (row > 1000) return -1; + + /* if lookupflag is not set, it's good (deleteflag is ok) */ + for (i = row; i > 0; i--) { + row_start = mdb_get_int16(mdb->pg_buf, (rco + i*2)); + if (!(row_start & 0x8000)) { + break; + } + } + + row_end = (i == 0) ? mdb->fmt->pg_size : row_start & OFFSET_MASK; +#endif + return row_end - 1; +} +int mdb_is_null(unsigned char *null_mask, int col_num) +{ +int byte_num = (col_num - 1) / 8; +int bit_num = (col_num - 1) % 8; + + if ((1 << bit_num) & null_mask[byte_num]) { + return 0; + } else { + return 1; + } +} +/* bool has to be handled specially because it uses the null bit to store its +** value*/ +static size_t +mdb_xfer_bound_bool(MdbHandle *mdb, MdbColumn *col, int value) +{ + col->cur_value_len = value; + if (col->bind_ptr) { + strcpy(col->bind_ptr, + value ? mdb->boolean_false_value : mdb->boolean_true_value); + } + if (col->len_ptr) { + *col->len_ptr = strlen(col->bind_ptr); + } + + return 1; +} +static size_t +mdb_xfer_bound_ole(MdbHandle *mdb, int start, MdbColumn *col, int len) +{ + size_t ret = 0; + if (len) { + col->cur_value_start = start; + col->cur_value_len = len; + } else { + col->cur_value_start = 0; + col->cur_value_len = 0; + } +#ifdef MDB_COPY_OLE + if (col->bind_ptr || col->len_ptr) { + ret = mdb_copy_ole(mdb, col->bind_ptr, start, len); + } +#else + if (col->bind_ptr) { + memcpy(col->bind_ptr, mdb->pg_buf + start, MDB_MEMO_OVERHEAD); + } + ret = MDB_MEMO_OVERHEAD; +#endif + if (col->len_ptr) { + *col->len_ptr = ret; + } + return ret; +} +static size_t +mdb_xfer_bound_data(MdbHandle *mdb, int start, MdbColumn *col, int len) +{ +int ret; + //if (!strcmp("Name",col->name)) { + //printf("start %d %d\n",start, len); + //} + if (len) { + col->cur_value_start = start; + col->cur_value_len = len; + } else { + col->cur_value_start = 0; + col->cur_value_len = 0; + } + if (col->bind_ptr) { + if (!len) { + strcpy(col->bind_ptr, ""); + } else { + //fprintf(stdout,"len %d size %d\n",len, col->col_size); + char *str; + if (col->col_type == MDB_NUMERIC) { + str = mdb_numeric_to_string(mdb, start, col->col_scale, col->col_prec); + } else if (col->col_type == MDB_DATETIME) { + if (mdb_col_is_shortdate(col)) { + str = mdb_date_to_string(mdb, mdb->shortdate_fmt, mdb->pg_buf, start); + } else { + str = mdb_date_to_string(mdb, mdb->date_fmt, mdb->pg_buf, start); + } + } else { + str = mdb_col_to_string(mdb, mdb->pg_buf, start, col->col_type, len); + } + snprintf(col->bind_ptr, mdb->bind_size, "%s", str); + g_free(str); + } + ret = strlen(col->bind_ptr); + if (col->len_ptr) { + *col->len_ptr = ret; + } + return ret; + } + return 0; +} +int mdb_read_row(MdbTableDef *table, unsigned int row) +{ + MdbHandle *mdb = table->entry->mdb; + MdbColumn *col; + unsigned int i; + int row_start; + size_t row_size = 0; + int delflag, lookupflag; + MdbField *fields; + int num_fields; + + if (table->num_cols == 0 || !table->columns) + return 0; + + if (mdb_find_row(mdb, row, &row_start, &row_size) == -1 || row_size == 0) { + /* Emitting a warning here isn't especially helpful. The row metadata + * could be bogus for a number of reasons, so just skip to the next one + * without comment. */ + // fprintf(stderr, "warning: mdb_find_row failed.\n"); + // fprintf(stderr, "warning: row_size = 0.\n"); + return 0; + } + + delflag = lookupflag = 0; + if (row_start & 0x8000) lookupflag++; + if (row_start & 0x4000) delflag++; + row_start &= OFFSET_MASK; /* remove flags */ +#if MDB_DEBUG + fprintf(stdout,"Row %d bytes %d to %d %s %s\n", + row, row_start, row_start + row_size - 1, + lookupflag ? "[lookup]" : "", + delflag ? "[delflag]" : ""); +#endif + + if (!table->noskip_del && delflag) { + return 0; + } + + fields = malloc(sizeof(MdbField) * table->num_cols); + + num_fields = mdb_crack_row(table, row_start, row_size, fields); + if (num_fields < 0 || !mdb_test_sargs(table, fields, num_fields)) { + free(fields); + return 0; + } + +#if MDB_DEBUG + fprintf(stdout,"sarg test passed row %d \n", row); +#endif + +#if MDB_DEBUG + mdb_buffer_dump(mdb->pg_buf, row_start, row_size); +#endif + + /* take advantage of mdb_crack_row() to clean up binding */ + /* use num_cols instead of num_fields -- bsb 03/04/02 */ + for (i = 0; i < table->num_cols; i++) { + col = g_ptr_array_index(table->columns,fields[i].colnum); + _mdb_attempt_bind(mdb, col, fields[i].is_null, + fields[i].start, fields[i].siz); + } + + free(fields); + + return 1; +} +static int _mdb_attempt_bind(MdbHandle *mdb, + MdbColumn *col, + unsigned char isnull, + int offset, + int len) +{ + if (col->col_type == MDB_BOOL) { + mdb_xfer_bound_bool(mdb, col, isnull); + } else if (isnull) { + mdb_xfer_bound_data(mdb, 0, col, 0); + } else if (col->col_type == MDB_OLE) { + mdb_xfer_bound_ole(mdb, offset, col, len); + } else { + //if (!mdb_test_sargs(mdb, col, offset, len)) { + //return 0; + //} + mdb_xfer_bound_data(mdb, offset, col, len); + } + return 1; +} + +/* Read next data page into mdb->pg_buf */ +int mdb_read_next_dpg(MdbTableDef *table) +{ + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + int next_pg; + +#ifndef SLOW_READ + while (1) { + next_pg = mdb_map_find_next(mdb, table->usage_map, + table->map_sz, table->cur_phys_pg); + if (next_pg < 0) + break; /* unknow map type: goto fallback */ + if (!next_pg) + return 0; + if ((guint32)next_pg == table->cur_phys_pg) + return 0; /* Infinite loop */ + + if (!mdb_read_pg(mdb, next_pg)) { + fprintf(stderr, "error: reading page %d failed.\n", next_pg); + return 0; + } + + table->cur_phys_pg = next_pg; + if (mdb->pg_buf[0]==MDB_PAGE_DATA && mdb_get_int32(mdb->pg_buf, 4)==(long)entry->table_pg) + return table->cur_phys_pg; + + /* On rare occasion, mdb_map_find_next will return a wrong page */ + /* Found in a big file, over 4,000,000 records */ + fprintf(stderr, + "warning: page %d from map doesn't match: Type=%d, buf[4..7]=%ld Expected table_pg=%ld\n", + next_pg, mdb->pg_buf[0], mdb_get_int32(mdb->pg_buf, 4), entry->table_pg); + } + fprintf(stderr, "Warning: defaulting to brute force read\n"); +#endif + /* can't do a fast read, go back to the old way */ + do { + if (!mdb_read_pg(mdb, table->cur_phys_pg++)) + return 0; + } while (mdb->pg_buf[0]!=MDB_PAGE_DATA || mdb_get_int32(mdb->pg_buf, 4)!=(long)entry->table_pg); + /* fprintf(stderr,"returning new page %ld\n", table->cur_phys_pg); */ + return table->cur_phys_pg; +} +int mdb_rewind_table(MdbTableDef *table) +{ + table->cur_pg_num=0; + table->cur_phys_pg=0; + table->cur_row=0; + + return 0; +} +int +mdb_fetch_row(MdbTableDef *table) +{ + MdbHandle *mdb = table->entry->mdb; + MdbFormatConstants *fmt = mdb->fmt; + unsigned int rows; + int rc; + guint32 pg; + + /* initialize */ + if (!table->cur_pg_num) { + table->cur_pg_num=1; + table->cur_row=0; + if ((!table->is_temp_table)&&(table->strategy!=MDB_INDEX_SCAN)) + if (!mdb_read_next_dpg(table)) return 0; + } + + do { + if (table->is_temp_table) { + GPtrArray *pages = table->temp_table_pages; + if (pages->len == 0) + return 0; + rows = mdb_get_int16( + g_ptr_array_index(pages, table->cur_pg_num-1), + fmt->row_count_offset); + if (table->cur_row >= rows) { + table->cur_row = 0; + if (++table->cur_pg_num > (unsigned int)pages->len) + return 0; + } + memcpy(mdb->pg_buf, + g_ptr_array_index(pages, table->cur_pg_num-1), + fmt->pg_size); + } else if (table->strategy==MDB_INDEX_SCAN) { + + if (!mdb_index_find_next(table->mdbidx, table->scan_idx, table->chain, &pg, (guint16 *) &(table->cur_row))) { + mdb_index_scan_free(table); + return 0; + } + mdb_read_pg(mdb, pg); + } else { + rows = mdb_get_int16(mdb->pg_buf,fmt->row_count_offset); + + /* if at end of page, find a new data page */ + if (table->cur_row >= rows) { + table->cur_row=0; + + if (!mdb_read_next_dpg(table)) { + return 0; + } + } + } + + /* printf("page %d row %d\n",table->cur_phys_pg, table->cur_row); */ + rc = mdb_read_row(table, table->cur_row); + table->cur_row++; + } while (!rc); + + return 1; +} +void mdb_data_dump(MdbTableDef *table) +{ + unsigned int i; + int ret; + char **bound_values = calloc(table->num_cols, sizeof(char *)); + + for (i=0;inum_cols;i++) { + bound_values[i] = g_malloc(MDB_BIND_SIZE); + ret = mdb_bind_column(table, i+1, bound_values[i], NULL); + if (ret == -1) { + fprintf(stderr, "error binding column %d\n", i+1); + g_free(bound_values[i]); + bound_values[i] = NULL; + } + } + mdb_rewind_table(table); + while (mdb_fetch_row(table)) { + for (i=0;inum_cols;i++) { + if (bound_values[i]) { + fprintf(stdout, "column %d is %s\n", i+1, bound_values[i]); + } + } + } + for (i=0;inum_cols;i++) { + g_free(bound_values[i]); + } + free(bound_values); +} + +int mdb_is_fixed_col(MdbColumn *col) +{ + return col->is_fixed; +} +#if 0 +static char *mdb_data_to_hex(MdbHandle *mdb, char *text, int start, int size) +{ +int i; + + for (i=start; ipg_buf[i]); + } + text[(i-start)*2]='\0'; + + return text; +} +#endif +/* + * ole_ptr should point to the original blob value of the field. + * If omited, there will be no multi-page check to that the caller is + * responsible for not calling this function. Then, it doesn't have to + * preserve the original value. + */ +size_t +mdb_ole_read_next(MdbHandle *mdb, MdbColumn *col, void *ole_ptr) +{ + guint32 ole_len; + void *buf; + int row_start; + size_t len; + + if (ole_ptr) { + ole_len = mdb_get_int32(ole_ptr, 0); + mdb_debug(MDB_DEBUG_OLE,"ole len = %d ole flags = %02x", + ole_len & 0x00ffffff, ole_len >> 24); + + if ((ole_len & 0x80000000) + || (ole_len & 0x40000000)) + /* inline or single-page fields don't have a next */ + return 0; + } + mdb_debug(MDB_DEBUG_OLE, "pg_row %d", col->cur_blob_pg_row); + if (!col->cur_blob_pg_row) + return 0; /* we are done */ + if (mdb_find_pg_row(mdb, col->cur_blob_pg_row, + &buf, &row_start, &len)) { + return 0; + } + if (len < 4) + return 0; + mdb_debug(MDB_DEBUG_OLE,"start %d len %d", row_start, len); + + if (col->bind_ptr) + memcpy(col->bind_ptr, (char*)buf + row_start + 4, len - 4); + col->cur_blob_pg_row = mdb_get_int32(buf, row_start); + + return len - 4; +} +size_t +mdb_ole_read(MdbHandle *mdb, MdbColumn *col, void *ole_ptr, size_t chunk_size) +{ + guint32 ole_len; + void *buf; + int row_start; + size_t len; + + ole_len = mdb_get_int32(ole_ptr, 0); + mdb_debug(MDB_DEBUG_OLE,"ole len = %d ole flags = %02x", + ole_len & 0x00ffffff, ole_len >> 24); + + col->chunk_size = chunk_size; + + if (ole_len & 0x80000000) { + /* inline ole field, if we can satisfy it, then do it */ + len = col->cur_value_len - MDB_MEMO_OVERHEAD; + if (chunk_size < len) + return 0; + if (col->bind_ptr) + memcpy(col->bind_ptr, &mdb->pg_buf[col->cur_value_start + + MDB_MEMO_OVERHEAD], len); + return len; + } else if (ole_len & 0x40000000) { + col->cur_blob_pg_row = mdb_get_int32(ole_ptr, 4); + mdb_debug(MDB_DEBUG_OLE,"ole row = %d ole pg = %ld", + col->cur_blob_pg_row & 0xff, + col->cur_blob_pg_row >> 8); + + if (mdb_find_pg_row(mdb, col->cur_blob_pg_row, + &buf, &row_start, &len)) { + return 0; + } + mdb_debug(MDB_DEBUG_OLE,"start %d len %d", row_start, len); + + if (col->bind_ptr) { + memcpy(col->bind_ptr, (char*)buf + row_start, len); + if (mdb_get_option(MDB_DEBUG_OLE)) + mdb_buffer_dump(col->bind_ptr, 0, 16); + } + return len; + } else if ((ole_len & 0xf0000000) == 0) { + col->cur_blob_pg_row = mdb_get_int32(ole_ptr, 4); + mdb_debug(MDB_DEBUG_OLE,"ole row = %d ole pg = %ld", + col->cur_blob_pg_row & 0xff, + col->cur_blob_pg_row >> 8); + + if (mdb_find_pg_row(mdb, col->cur_blob_pg_row, + &buf, &row_start, &len) || len < 4) { + return 0; + } + mdb_debug(MDB_DEBUG_OLE,"start %d len %d", row_start, len); + + if (col->bind_ptr) + memcpy(col->bind_ptr, (char*)buf + row_start + 4, len - 4); + col->cur_blob_pg_row = mdb_get_int32(buf, row_start); + mdb_debug(MDB_DEBUG_OLE, "next pg_row %d", col->cur_blob_pg_row); + + return len - 4; + } else { + fprintf(stderr,"Unhandled ole field flags = %02x\n", ole_len >> 24); + return 0; + } +} +/* + * mdb_ole_read_full calls mdb_ole_read then loop over mdb_ole_read_next as much as necessary. + * returns the result in a big buffer. + * The call must free it. + * Note that this function is not idempotent: It may be called only once per column after each bind. + */ +void* +mdb_ole_read_full(MdbHandle *mdb, MdbColumn *col, size_t *size) +{ + char ole_ptr[MDB_MEMO_OVERHEAD]; + char *result = malloc(OLE_BUFFER_SIZE); + size_t result_buffer_size = OLE_BUFFER_SIZE; + size_t len, pos; + + memcpy(ole_ptr, col->bind_ptr, MDB_MEMO_OVERHEAD); + + len = mdb_ole_read(mdb, col, ole_ptr, OLE_BUFFER_SIZE); + memcpy(result, col->bind_ptr, len); + pos = len; + while ((len = mdb_ole_read_next(mdb, col, ole_ptr))) { + if (pos+len >= result_buffer_size) { + result_buffer_size += OLE_BUFFER_SIZE; + if ((result = reallocf(result, result_buffer_size)) == NULL) { + fprintf(stderr, "Out of memory while reading OLE object\n"); + return NULL; + } + } + memcpy(result + pos, col->bind_ptr, len); + pos += len; + } + if (size) + *size = pos; + return result; +} + +#ifdef MDB_COPY_OLE +static size_t mdb_copy_ole(MdbHandle *mdb, void *dest, int start, int size) +{ + guint32 ole_len; + gint32 row_start, pg_row; + size_t len; + void *buf, *pg_buf = mdb->pg_buf; + + if (size> 8); + + if (mdb_find_pg_row(mdb, pg_row, &buf, &row_start, &len)) { + return 0; + } + mdb_debug(MDB_DEBUG_OLE,"row num %d start %d len %d", + pg_row & 0xff, row_start, len); + + if (dest) + memcpy(dest, buf + row_start, len); + return len; + } else if ((ole_len & 0xff000000) == 0) { // assume all flags in MSB + /* multi-page */ + int cur = 0; + pg_row = mdb_get_int32(pg_buf, start+4); + do { + mdb_debug(MDB_DEBUG_OLE,"Reading LVAL page %06x", + pg_row >> 8); + + if (mdb_find_pg_row(mdb,pg_row,&buf,&row_start,&len) || len < 4) { + return 0; + } + + mdb_debug(MDB_DEBUG_OLE,"row num %d start %d len %d", + pg_row & 0xff, row_start, len); + + if (dest) + memcpy(dest+cur, buf + row_start + 4, len - 4); + cur += len - 4; + + /* find next lval page */ + pg_row = mdb_get_int32(buf, row_start); + } while ((pg_row >> 8)); + return cur; + } else { + fprintf(stderr, "Unhandled ole field flags = %02x\n", ole_len >> 24); + return 0; + } +} +#endif +static char *mdb_memo_to_string(MdbHandle *mdb, int start, int size) +{ + guint32 memo_len; + gint32 row_start, pg_row; + size_t len; + void *buf, *pg_buf = mdb->pg_buf; + char *text = g_malloc(mdb->bind_size); + + if (sizebind_size); + return text; + } else if (memo_len & 0x40000000) { + /* single-page memo field */ + pg_row = mdb_get_int32(pg_buf, start+4); +#if MDB_DEBUG + printf("Reading LVAL page %06x\n", pg_row >> 8); +#endif + if (mdb_find_pg_row(mdb, pg_row, &buf, &row_start, &len)) { + strcpy(text, ""); + return text; + } +#if MDB_DEBUG + printf("row num %d start %d len %d\n", + pg_row & 0xff, row_start, len); + mdb_buffer_dump(buf, row_start, len); +#endif + mdb_unicode2ascii(mdb, (char*)buf + row_start, len, text, mdb->bind_size); + return text; + } else if ((memo_len & 0xff000000) == 0) { // assume all flags in MSB + /* multi-page memo field */ + guint32 tmpoff = 0; + char *tmp; + + tmp = g_malloc(memo_len); + pg_row = mdb_get_int32(pg_buf, start+4); + do { +#if MDB_DEBUG + printf("Reading LVAL page %06x\n", pg_row >> 8); +#endif + if (mdb_find_pg_row(mdb,pg_row,&buf,&row_start,&len)) { + g_free(tmp); + strcpy(text, ""); + return text; + } +#if MDB_DEBUG + printf("row num %d start %d len %d\n", + pg_row & 0xff, row_start, len); +#endif + if (tmpoff + len - 4 > memo_len) + break; + + /* Stop processing on zero length multiple page memo fields */ + if (len < 4) + break; + + memcpy(tmp + tmpoff, (char*)buf + row_start + 4, len - 4); + tmpoff += len - 4; + } while (( pg_row = mdb_get_int32(buf, row_start) )); + if (tmpoff < memo_len) { + fprintf(stderr, "Warning: incorrect memo length\n"); + } + mdb_unicode2ascii(mdb, tmp, tmpoff, text, mdb->bind_size); + g_free(tmp); + return text; + } else { + fprintf(stderr, "Unhandled memo field flags = %02x\n", memo_len >> 24); + strcpy(text, ""); + return text; + } +} + +#if 0 +static int trim_trailing_zeros(char * buff) +{ + char *p; + int n = strlen(buff); + + /* Don't need to trim strings with no decimal portion */ + if(!strchr(buff,'.')) + return 0; + + /* Trim the zeros */ + p = buff + n - 1; + while (p >= buff && *p == '0') + *p-- = '\0'; + + /* If a decimal sign is left at the end, remove it too */ + if (*p == '.') + *p = '\0'; + + return 0; +} +#endif + + +/* Date/Time is stored as a double, where the whole + part is the days from 12/30/1899 and the fractional + part is the fractional part of one day. */ + +void +mdb_tm_to_date(struct tm *t, double *td) +{ + short yr = t->tm_year + 1900; + char leap = ((yr & 3) == 0) && ((yr % 100) != 0 || (yr % 400) == 0); + const int *cal = leap ? leap_cal : noleap_cal; + long int time = (yr*365+(yr/4)-(yr/100)+(yr/400)+cal[t->tm_mon]+t->tm_mday)-693959; + + *td = (((long)t->tm_hour * 3600)+((long)t->tm_min * 60)+((long)t->tm_sec)) / 86400.0; + if (time>=0) *td+=time; else *td=time-*td; +} + +void +mdb_date_to_tm(double td, struct tm *t) +{ + long day, time; + long yr, q; + const int *cal; + + if (td < 0.0 || td > 1e6) // About 2700 AD + return; + + yr = 1; + day = (long)(td); + time = (long)((td - day) * 86400.0 + 0.5); + t->tm_hour = time / 3600; + t->tm_min = (time / 60) % 60; + t->tm_sec = time % 60; + + day += 693593; /* Days from 1/1/1 to 12/31/1899 */ + t->tm_wday = (day+1) % 7; + + q = day / 146097; /* 146097 days in 400 years */ + yr += 400 * q; + day -= q * 146097; + + q = day / 36524; /* 36524 days in 100 years */ + if (q > 3) q = 3; + yr += 100 * q; + day -= q * 36524; + + q = day / 1461; /* 1461 days in 4 years */ + yr += 4 * q; + day -= q * 1461; + + q = day / 365; /* 365 days in 1 year */ + if (q > 3) q = 3; + yr += q; + day -= q * 365; + + cal = ((yr)%4==0 && ((yr)%100!=0 || (yr)%400==0)) ? + leap_cal : noleap_cal; + for (t->tm_mon=0; t->tm_mon<12; t->tm_mon++) { + if (day < cal[t->tm_mon+1]) break; + } + t->tm_year = yr - 1900; + t->tm_mday = day - cal[t->tm_mon] + 1; + t->tm_yday = day; + t->tm_isdst = -1; +} + +static char * +mdb_date_to_string(MdbHandle *mdb, const char *fmt, void *buf, int start) +{ + struct tm t = { 0 }; + char *text = g_malloc(mdb->bind_size); + double td = mdb_get_double(buf, start); + + mdb_date_to_tm(td, &t); + + strftime(text, mdb->bind_size, mdb->date_fmt, &t); + + return text; +} + +char *mdb_uuid_to_string(const void *buf, int pos) +{ + return mdb_uuid_to_string_fmt(buf,pos,MDB_BRACES_4_2_2_8); +} + +char *mdb_uuid_to_string_fmt(const void *buf, int pos, MdbUuidFormat format) +{ + const unsigned char *kkd = (const unsigned char *)buf; + return g_strdup_printf(format == MDB_BRACES_4_2_2_8 + ? "{%02X%02X%02X%02X" "-" "%02X%02X" "-" "%02X%02X" "-" "%02X%02X%02X%02X%02X%02X%02X%02X}" + : "%02X%02X%02X%02X" "-" "%02X%02X" "-" "%02X%02X" "-" "%02X%02X" "-" "%02X%02X%02X%02X%02X%02X", + kkd[pos+3], kkd[pos+2], kkd[pos+1], kkd[pos], // little-endian + kkd[pos+5], kkd[pos+4], // little-endian + kkd[pos+7], kkd[pos+6], // little-endian + kkd[pos+8], kkd[pos+9], // big-endian + kkd[pos+10], kkd[pos+11], + kkd[pos+12], kkd[pos+13], + kkd[pos+14], kkd[pos+15]); // big-endian +} + +#if 0 +int floor_log10(double f, int is_single) +{ + unsigned int i; + double y = 10.0; + + if (f < 0.0) + f = -f; + + if ((f == 0.0) || (f == 1.0) || isinf(f)) { + return 0; + } else if (f < 1.0) { + if (is_single) { + /* The intermediate value p is necessary to prevent + * promotion of the comparison to type double */ + float p; + for (i=1; (p = f * y) < 1.0; i++) + y *= 10.0; + } else { + for (i=1; f * y < 1.0; i++) + y *= 10.0; + } + return -(int)i; + } else { /* (x > 1.0) */ + for (i=0; f >= y; i++) + y *= 10.0; + return (int)i; + } +} +#endif + +char *mdb_col_to_string(MdbHandle *mdb, void *buf, int start, int datatype, int size) +{ + char *text = NULL; + float tf; + double td; + + switch (datatype) { + case MDB_BYTE: + text = g_strdup_printf("%hhu", mdb_get_byte(buf, start)); + break; + case MDB_INT: + text = g_strdup_printf("%hd", + (short)mdb_get_int16(buf, start)); + break; + case MDB_LONGINT: + case MDB_COMPLEX: + text = g_strdup_printf("%d", + (int)mdb_get_int32(buf, start)); + break; + case MDB_FLOAT: + tf = mdb_get_single(buf, start); + text = g_strdup_printf("%.8g", tf); + break; + case MDB_DOUBLE: + td = mdb_get_double(buf, start); + text = g_strdup_printf("%.16lg", td); + break; + case MDB_BINARY: + if (size<0) { + text = g_strdup(""); + } else { + text = g_malloc(size+1); + memcpy(text, (char*)buf+start, size); + text[size] = '\0'; + } + break; + case MDB_TEXT: + if (size<0) { + text = g_strdup(""); + } else { + text = g_malloc(mdb->bind_size); + mdb_unicode2ascii(mdb, (char*)buf + start, + size, text, mdb->bind_size); + } + break; + case MDB_DATETIME: + text = mdb_date_to_string(mdb, mdb->date_fmt, buf, start); + break; + case MDB_MEMO: + text = mdb_memo_to_string(mdb, start, size); + break; + case MDB_MONEY: + text = mdb_money_to_string(mdb, start); + break; + case MDB_REPID: + text = mdb_uuid_to_string_fmt(buf, start, mdb->repid_fmt); + break; + default: + /* shouldn't happen. bools are handled specially + ** by mdb_xfer_bound_bool() */ + fprintf(stderr, "Warning: mdb_col_to_string called on unsupported data type %d.\n", datatype); + text = g_strdup(""); + break; + } + return text; +} +int mdb_col_disp_size(MdbColumn *col) +{ + switch (col->col_type) { + case MDB_BOOL: + return 1; + break; + case MDB_BYTE: + return 4; + break; + case MDB_INT: + return 6; + break; + case MDB_LONGINT: + case MDB_COMPLEX: + return 11; + break; + case MDB_FLOAT: + return 10; + break; + case MDB_DOUBLE: + return 10; + break; + case MDB_TEXT: + return col->col_size; + break; + case MDB_DATETIME: + return 20; + break; + case MDB_MEMO: + return 64000; + break; + case MDB_MONEY: + return 21; + break; + } + return 0; +} +int mdb_col_fixed_size(MdbColumn *col) +{ + switch (col->col_type) { + case MDB_BOOL: + return 1; + break; + case MDB_BYTE: + return -1; + break; + case MDB_INT: + return 2; + break; + case MDB_LONGINT: + case MDB_COMPLEX: + return 4; + break; + case MDB_FLOAT: + return 4; + break; + case MDB_DOUBLE: + return 8; + break; + case MDB_TEXT: + return -1; + break; + case MDB_DATETIME: + return 4; + break; + case MDB_BINARY: + return -1; + break; + case MDB_MEMO: + return -1; + break; + case MDB_MONEY: + return 8; + break; + } + return 0; +} diff --git a/wlx/dbview/src/libmdb/dump.c b/wlx/dbview/src/libmdb/dump.c new file mode 100644 index 0000000..121dc1d --- /dev/null +++ b/wlx/dbview/src/libmdb/dump.c @@ -0,0 +1,57 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2000-2011 Brian Bruns and others + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include + +void mdb_buffer_dump(const void* buf, off_t start, size_t len) +{ + char asc[20]; + size_t j; + int k = 0; + + memset(asc, 0, sizeof(asc)); + k = 0; + for (j=0; j +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_ICONV +#include +#endif + +/* Linked from libmdb */ +const char *mdb_iconv_name_from_code_page(int code_page); + +/* string functions */ + +void *g_memdup(const void *src, size_t len) { + void *dst = malloc(len); + memcpy(dst, src, len); + return dst; +} + +int g_str_equal(const void *str1, const void *str2) { + return strcmp(str1, str2) == 0; +} + +// max_tokens not yet implemented +char **g_strsplit(const char *haystack, const char *needle, int max_tokens) { + char **ret = NULL; + char *found = NULL; + size_t components = 2; // last component + terminating NULL + + while ((found = strstr(haystack, needle))) { + components++; + haystack = found + strlen(needle); + } + + ret = calloc(components, sizeof(char *)); + + int i = 0; + while ((found = strstr(haystack, needle))) { + ret[i++] = g_strndup(haystack, found - haystack); + haystack = found + strlen(needle); + } + ret[i] = strdup(haystack); + + return ret; +} + +void g_strfreev(char **dir) { + int i=0; + while (dir[i]) { + free(dir[i]); + i++; + } + free(dir); +} + +char *g_strconcat(const char *first, ...) { + char *ret = NULL; + size_t len = strlen(first); + char *arg = NULL; + va_list argp; + + va_start(argp, first); + while ((arg = va_arg(argp, char *))) { + len += strlen(arg); + } + va_end(argp); + + ret = malloc(len+1); + + char *pos = strcpy(ret, first) + strlen(first); + + va_start(argp, first); + while ((arg = va_arg(argp, char *))) { + pos = strcpy(pos, arg) + strlen(arg); + } + va_end(argp); + + ret[len] = '\0'; + + return ret; +} + +#if defined _WIN32 && !defined(HAVE_VASPRINTF) && !defined(HAVE_VASNPRINTF) +int vasprintf(char **ret, const char *format, va_list ap) { + int len; + int retval; + char *result; + if ((len = _vscprintf(format, ap)) < 0) + return -1; + if ((result = malloc(len+1)) == NULL) + return -1; + if ((retval = vsprintf_s(result, len+1, format, ap)) == -1) { + free(result); + return -1; + } + *ret = result; + return retval; +} +#endif + +char *g_strdup(const char *input) { + size_t len = strlen(input); + return g_memdup(input, len+1); +} + +char *g_strndup(const char *src, size_t len) { + if (!src) + return NULL; + char *result = malloc(len+1); + size_t i=0; + while (*src && istr = strdup(init ? init : ""); + str->len = strlen(str->str); + str->allocated_len = str->len+1; + return str; +} + +GString *g_string_assign(GString *string, const gchar *rval) { + size_t len = strlen(rval); + string->str = realloc(string->str, len+1); + strncpy(string->str, rval, len+1); + string->len = len; + string->allocated_len = len+1; + return string; +} + +GString * g_string_append (GString *string, const gchar *val) { + size_t len = strlen(val); + string->str = realloc(string->str, string->len + len + 1); + strncpy(&string->str[string->len], val, len+1); + string->len += len; + string->allocated_len = string->len + len + 1; + return string; +} + +gchar *g_string_free (GString *string, gboolean free_segment) { + char *data = string->str; + free(string); + if (free_segment) { + free(data); + return NULL; + } + return data; +} + +/* conversion */ +gint g_unichar_to_utf8(gunichar u, gchar *dst) { + if (u >= 0x0800) { + *dst++ = 0xE0 | ((u & 0xF000) >> 12); + *dst++ = 0x80 | ((u & 0x0FC0) >> 6); + *dst++ = 0x80 | ((u & 0x003F) >> 0); + return 3; + } + if (u >= 0x0080) { + *dst++ = 0xC0 | ((u & 0x07C0) >> 6); + *dst++ = 0x80 | ((u & 0x003F) >> 0); + return 2; + } + *dst++ = (u & 0x7F); + return 1; +} + +gchar *g_locale_to_utf8(const gchar *opsysstring, size_t len, + size_t *bytes_read, size_t *bytes_written, GError **error) { + if (len == (size_t)-1) + len = strlen(opsysstring); + size_t wlen = mbstowcs(NULL, opsysstring, 0); + if (wlen == (size_t)-1) { + if (error) { + *error = malloc(sizeof(GError)); + (*error)->message = g_strdup_printf("Invalid multibyte string: %s\n", opsysstring); + } + return NULL; + } + wchar_t *utf16 = malloc(sizeof(wchar_t)*(wlen+1)); + mbstowcs(utf16, opsysstring, wlen+1); + gchar *utf8 = malloc(3*len+1); + gchar *dst = utf8; + size_t i; + for (i=0; i= 0x10000 requires surrogate pairs, ignore + dst += g_unichar_to_utf8(utf16[i], dst); + } + *dst++ = '\0'; + free(utf16); + return utf8; +} + +gchar *g_utf8_casefold(const gchar *str, gssize len) { + return g_utf8_strdown(str, len); +} + +gchar *g_utf8_strdown(const gchar *str, gssize len) { + gssize i = 0; + if (len == -1) + len = strlen(str); + gchar *lower = malloc(len+1); + while (iarray->len; i++) { + MyNode *node = g_ptr_array_index(table->array, i); + if (table->compare(key, node->key)) + return node->value; + } + return NULL; +} + +gboolean g_hash_table_lookup_extended (GHashTable *table, const void *lookup_key, + void **orig_key, void **value) { + guint i; + for (i=0; iarray->len; i++) { + MyNode *node = g_ptr_array_index(table->array, i); + if (table->compare(lookup_key, node->key)) { + *orig_key = node->key; + *value = node->value; + return TRUE; + } + } + return FALSE; +} + +void g_hash_table_insert(GHashTable *table, void *key, void *value) { + MyNode *node = calloc(1, sizeof(MyNode)); + node->value = value; + node->key = key; + g_ptr_array_add(table->array, node); +} + +gboolean g_hash_table_remove(GHashTable *table, gconstpointer key) { + int found = 0; + guint i; + for (i=0; iarray->len; i++) { + MyNode *node = g_ptr_array_index(table->array, i); + if (found) { + table->array->pdata[i-1] = table->array->pdata[i]; + } else if (!found && table->compare(key, node->key)) { + found = 1; + } + } + if (found) { + table->array->len--; + } + return found; +} + +GHashTable *g_hash_table_new(GHashFunc hashes, GEqualFunc equals) { + GHashTable *table = calloc(1, sizeof(GHashTable)); + table->array = g_ptr_array_new(); + table->compare = equals; + return table; +} + +void g_hash_table_foreach(GHashTable *table, GHFunc function, void *data) { + guint i; + for (i=0; iarray->len; i++) { + MyNode *node = g_ptr_array_index(table->array, i); + function(node->key, node->value, data); + } +} + +void g_hash_table_destroy(GHashTable *table) { + guint i; + for (i=0; iarray->len; i++) { + MyNode *node = g_ptr_array_index(table->array, i); + free(node); + } + g_ptr_array_free(table->array, TRUE); + free(table); +} + +/* GPtrArray */ + +void g_ptr_array_sort(GPtrArray *array, GCompareFunc func) { + qsort(array->pdata, array->len, sizeof(void *), func); +} + +void g_ptr_array_foreach(GPtrArray *array, GFunc function, gpointer user_data) { + guint i; + for (i=0; ilen; i++) { + function(g_ptr_array_index(array, i), user_data); + } +} + +GPtrArray *g_ptr_array_new() { + GPtrArray *array = malloc(sizeof(GPtrArray)); + array->len = 0; + array->pdata = NULL; + return array; +} + +void g_ptr_array_add(GPtrArray *array, void *entry) { + array->pdata = realloc(array->pdata, (array->len+1) * sizeof(void *)); + array->pdata[array->len++] = entry; +} + +gboolean g_ptr_array_remove(GPtrArray *array, gpointer data) { + int found = 0; + guint i; + for (i=0; ilen; i++) { + if (found) { + array->pdata[i-1] = array->pdata[i]; + } else if (!found && array->pdata[i] == data) { + found = 1; + } + } + if (found) { + array->len--; + } + return found; +} + +void g_ptr_array_free(GPtrArray *array, gboolean something) { + free(array->pdata); + free(array); +} + +/* GList */ + +GList *g_list_append(GList *list, void *data) { + GList *new_list = calloc(1, sizeof(GList)); + new_list->data = data; + new_list->next = list; + if (list) + list->prev = new_list; + return new_list; +} + +GList *g_list_last(GList *list) { + while (list && list->next) { + list = list->next; + } + return list; +} + +GList *g_list_remove(GList *list, void *data) { + GList *link = list; + while (link) { + if (link->data == data) { + GList *return_list = list; + if (link->prev) + link->prev->next = link->next; + if (link->next) + link->next->prev = link->prev; + if (link == list) + return_list = link->next; + free(link); + return return_list; + } + link = link->next; + } + return list; +} + +void g_list_free(GList *list) { + GList *next = NULL; + while (list) { + next = list->next; + free(list); + list = next; + } +} + +/* GOption */ + +void g_option_context_add_main_entries (GOptionContext *context, + const GOptionEntry *entries, + const gchar *translation_domain) { + context->entries = entries; +} + +gchar *g_option_context_get_help (GOptionContext *context, + gboolean main_help, void *group) { +#if defined(__APPLE__) || defined(__FreeBSD__) + const char * appname = getprogname(); +#elif HAVE_DECL_PROGRAM_INVOCATION_SHORT_NAME + const char * appname = program_invocation_short_name; +#else + const char * appname = "mdb-util"; +#endif + + char *help = malloc(4096); + char *end = help + 4096; + char *p = help; + p += snprintf(p, end - p, + "Usage:\n %s [OPTION\xE2\x80\xA6] %s\n\n", appname, context->desc); + p += snprintf(p, end - p, + "Help Options:\n -h, --%-20s%s\n\n", "help", "Show help options"); + p += snprintf(p, end - p, + "Application Options:\n"); + int i=0; + for (i=0; context->entries[i].long_name; i++) { + p += snprintf(p, end - p, " "); + if (context->entries[i].short_name) { + p += snprintf(p, end - p, "-%c, ", context->entries[i].short_name); + } + p += snprintf(p, end - p, "--"); + if (context->entries[i].arg_description) { + char *long_name = g_strconcat( + context->entries[i].long_name, "=", + context->entries[i].arg_description, NULL); + p += snprintf(p, end - p, "%-20s", long_name); + free(long_name); + } else { + p += snprintf(p, end - p, "%-20s", context->entries[i].long_name); + } + if (!context->entries[i].short_name) { + p += snprintf(p, end - p, " "); + } + p += snprintf(p, end - p, "%s\n", context->entries[i].description); + } + p += snprintf(p, end - p, "\n"); + return help; +} + +GOptionContext *g_option_context_new(const char *description) { + GOptionContext *ctx = calloc(1, sizeof(GOptionContext)); + ctx->desc = description; + return ctx; +} + +gboolean g_option_context_parse(GOptionContext *context, + gint *argc, gchar ***argv, GError **error) { + int i; + int count = 0; + int len = 0; + if (*argc == 2 && + (strcmp((*argv)[1], "-h") == 0 || strcmp((*argv)[1], "--help") == 0)) { + fprintf(stderr, "%s", g_option_context_get_help(context, TRUE, NULL)); + exit(0); + } + for (i=0; context->entries[i].long_name; i++) { + GOptionArg arg = context->entries[i].arg; + count++; + len++; + if (arg != G_OPTION_ARG_NONE) + len++; + } + struct option *long_opts = calloc(count+1, sizeof(struct option)); + char *short_opts = calloc(1, len+1); + int j=0; + for (i=0; ientries[i]; + GOptionArg arg = entry->arg; + short_opts[j++] = entry->short_name; + if (arg != G_OPTION_ARG_NONE) + short_opts[j++] = ':'; + long_opts[i].name = entry->long_name; + long_opts[i].has_arg = entry->arg == G_OPTION_ARG_NONE ? no_argument : required_argument; + } + int c; + int longindex = 0; + opterr = 0; + while ((c = getopt_long(*argc, *argv, short_opts, long_opts, &longindex)) != -1) { + if (c == '?') { + *error = malloc(sizeof(GError)); + if (optopt) { + (*error)->message = g_strdup_printf("Unrecognized option: -%c", optopt); + } else { + (*error)->message = g_strdup_printf("Unrecognized option: %s", (*argv)[optind-1]); + } + free(short_opts); + free(long_opts); + return FALSE; + } + const GOptionEntry *entry = NULL; + if (c == 0) { + entry = &context->entries[longindex]; + } else { + for (i=0; ientries[i].short_name == c) { + entry = &context->entries[i]; + break; + } + } + } + if (entry->arg == G_OPTION_ARG_NONE) { + *(int *)entry->arg_data = !(entry->flags & G_OPTION_FLAG_REVERSE); + } else if (entry->arg == G_OPTION_ARG_INT) { + char *endptr = NULL; + *(int *)entry->arg_data = strtol(optarg, &endptr, 10); + if (*endptr) { + *error = malloc(sizeof(GError)); + (*error)->message = malloc(100); + snprintf((*error)->message, 100, "Argument to --%s must be an integer", entry->long_name); + free(short_opts); + free(long_opts); + return FALSE; + } + } else if (entry->arg == G_OPTION_ARG_FILENAME) { + *(char **)entry->arg_data = strdup(optarg); + } else if (entry->arg == G_OPTION_ARG_STRING) { + char *result = g_locale_to_utf8(optarg, -1, NULL, NULL, error); + if (result == NULL) { + free(short_opts); + free(long_opts); + return FALSE; + } + *(char **)entry->arg_data = result; + } + } + *argc -= (optind - 1); + *argv += (optind - 1); + free(short_opts); + free(long_opts); + + return TRUE; +} + +void g_option_context_free(GOptionContext *context) { + free(context); +} diff --git a/wlx/dbview/src/libmdb/file.c b/wlx/dbview/src/libmdb/file.c new file mode 100644 index 0000000..c6d0d83 --- /dev/null +++ b/wlx/dbview/src/libmdb/file.c @@ -0,0 +1,501 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include "mdbtools.h" +#include "mdbprivate.h" + +MdbFormatConstants MdbJet4Constants = { + .pg_size = 4096, + .row_count_offset = 0x0c, + .tab_num_rows_offset = 16, + .tab_num_cols_offset = 45, + .tab_num_idxs_offset = 47, + .tab_num_ridxs_offset = 51, + .tab_usage_map_offset = 55, + .tab_first_dpg_offset = 56, + .tab_cols_start_offset = 63, + .tab_ridx_entry_size = 12, + .col_scale_offset = 11, + .col_prec_offset = 12, + .col_flags_offset = 15, + .col_size_offset = 23, + .col_num_offset = 5, + .tab_col_entry_size = 25, + .tab_free_map_offset = 59, + .tab_col_offset_var = 7, + .tab_col_offset_fixed = 21, + .tab_row_col_num_offset = 9 +}; +MdbFormatConstants MdbJet3Constants = { + .pg_size = 2048, + .row_count_offset = 0x08, + .tab_num_rows_offset = 12, + .tab_num_cols_offset = 25, + .tab_num_idxs_offset = 27, + .tab_num_ridxs_offset = 31, + .tab_usage_map_offset = 35, + .tab_first_dpg_offset = 36, + .tab_cols_start_offset = 43, + .tab_ridx_entry_size = 8, + .col_scale_offset = 9, + .col_prec_offset = 10, + .col_flags_offset = 13, + .col_size_offset = 16, + .col_num_offset = 1, + .tab_col_entry_size = 18, + .tab_free_map_offset = 39, + .tab_col_offset_var = 3, + .tab_col_offset_fixed = 14, + .tab_row_col_num_offset = 5 +}; + +static ssize_t _mdb_read_pg(MdbHandle *mdb, void *pg_buf, unsigned long pg); + +/** + * mdb_find_file: + * @filename: path to MDB (database) file + * + * Finds and returns the absolute path to an MDB file. Function will first try + * to fstat file as passed, then search through the $MDBPATH if not found. + * + * Return value: gchar pointer to absolute path. Caller is responsible for + * freeing. + **/ + +static char *mdb_find_file(const char *file_name) +{ + struct stat status; + gchar *mdbpath, **dir, *tmpfname; + unsigned int i = 0; + + /* try the provided file name first */ + if (!stat(file_name, &status)) { + char *result; + result = g_strdup(file_name); + if (!result) + fprintf(stderr, "Can't alloc filename\n"); + return result; + } + + /* Now pull apart $MDBPATH and try those */ + mdbpath = (gchar *) getenv("MDBPATH"); + /* no path, can't find file */ + if (!mdbpath || !strlen(mdbpath)) return NULL; + + dir = g_strsplit(mdbpath, ":", 0); + while (dir[i]) { + if (!strlen(dir[i])) continue; + tmpfname = g_strconcat(dir[i++], "/", file_name, NULL); + if (!stat(tmpfname, &status)) { + g_strfreev(dir); + return tmpfname; + } + g_free(tmpfname); + } + g_strfreev(dir); + return NULL; +} + +/** + * mdb_handle_from_stream: + * @stream An open file stream + * @flags MDB_NOFLAGS for read-only, MDB_WRITABLE for read/write + * + * Allocates, initializes, and return an MDB handle from a file stream pointing + * to an MDB file. + * + * Return value: The handle on success, NULL on failure + */ +static MdbHandle *mdb_handle_from_stream(FILE *stream, MdbFileFlags flags) { + MdbHandle *mdb = g_malloc0(sizeof(MdbHandle)); + mdb_set_default_backend(mdb, "access"); + mdb_set_date_fmt(mdb, "%x %X"); + mdb_set_shortdate_fmt(mdb, "%x"); + mdb_set_bind_size(mdb, MDB_BIND_SIZE); + mdb_set_boolean_fmt_numbers(mdb); + mdb_set_repid_fmt(mdb, MDB_BRACES_4_2_2_8); +#ifdef HAVE_ICONV + mdb->iconv_in = (iconv_t)-1; + mdb->iconv_out = (iconv_t)-1; +#endif + /* need something to bootstrap with, reassign after page 0 is read */ + mdb->fmt = &MdbJet3Constants; + mdb->f = g_malloc0(sizeof(MdbFile)); + mdb->f->refs = 1; + mdb->f->stream = stream; + if (flags & MDB_WRITABLE) { + mdb->f->writable = TRUE; + } + + if (!mdb_read_pg(mdb, 0)) { + // fprintf(stderr,"Couldn't read first page.\n"); + mdb_close(mdb); + return NULL; + } + if (mdb->pg_buf[0] != 0) { + mdb_close(mdb); + return NULL; + } + mdb->f->jet_version = mdb_get_byte(mdb->pg_buf, 0x14); + switch(mdb->f->jet_version) { + case MDB_VER_JET3: + mdb->fmt = &MdbJet3Constants; + break; + case MDB_VER_JET4: + case MDB_VER_ACCDB_2007: + case MDB_VER_ACCDB_2010: + case MDB_VER_ACCDB_2013: + case MDB_VER_ACCDB_2016: + case MDB_VER_ACCDB_2019: + mdb->fmt = &MdbJet4Constants; + break; + default: + fprintf(stderr,"Unknown Jet version: %x\n", mdb->f->jet_version); + mdb_close(mdb); + return NULL; + } + + unsigned char tmp_key[4] = { 0xC7, 0xDA, 0x39, 0x6B }; + mdbi_rc4(tmp_key, sizeof(tmp_key), + mdb->pg_buf + 0x18, + mdb->f->jet_version == MDB_VER_JET3 ? 126 : 128 + ); + + if (mdb->f->jet_version == MDB_VER_JET3) { + mdb->f->lang_id = mdb_get_int16(mdb->pg_buf, 0x3a); + } else { + mdb->f->lang_id = mdb_get_int16(mdb->pg_buf, 0x6e); + } + mdb->f->code_page = mdb_get_int16(mdb->pg_buf, 0x3c); + mdb->f->db_key = mdb_get_int32(mdb->pg_buf, 0x3e); + if (mdb->f->jet_version == MDB_VER_JET3) { + /* JET4 needs additional masking with the DB creation date, currently unsupported */ + /* Bug - JET3 supports 20 byte passwords, this is currently just 14 bytes */ + memcpy(mdb->f->db_passwd, mdb->pg_buf + 0x42, sizeof(mdb->f->db_passwd)); + } + + mdb_iconv_init(mdb); + + return mdb; +} + +/** + * mdb_open_buffer: + * @buffer A memory buffer containing an MDB file + * @len Length of the buffer + * + * Opens an MDB file in memory and returns an MdbHandle to it. + * + * Return value: point to MdbHandle structure. + */ +MdbHandle *mdb_open_buffer(void *buffer, size_t len, MdbFileFlags flags) { + FILE *file = NULL; +#ifdef HAVE_FMEMOPEN + file = fmemopen(buffer, len, (flags & MDB_WRITABLE) ? "r+" : "r"); +#else + fprintf(stderr, "mdb_open_buffer requires a platform with support for fmemopen(3)\n"); +#endif + if (file == NULL) { + fprintf(stderr, "Couldn't open memory buffer\n"); + return NULL; + } + return mdb_handle_from_stream(file, flags); +} + +/** + * mdb_open: + * @filename: path to MDB (database) file + * @flags: MDB_NOFLAGS for read-only, MDB_WRITABLE for read/write + * + * Opens an MDB file and returns an MdbHandle to it. MDB File may be relative + * to the current directory, a full path to the file, or relative to a + * component of $MDBPATH. + * + * Return value: pointer to MdbHandle structure. + **/ +MdbHandle *mdb_open(const char *filename, MdbFileFlags flags) +{ + FILE *file; + + char *filepath = mdb_find_file(filename); + if (!filepath) { + fprintf(stderr, "File not found\n"); + return NULL; + } +#ifdef _WIN32 + char *mode = (flags & MDB_WRITABLE) ? "rb+" : "rb"; +#else + char *mode = (flags & MDB_WRITABLE) ? "r+" : "r"; +#endif + + if ((file = fopen(filepath, mode)) == NULL) { + fprintf(stderr,"Couldn't open file %s\n",filepath); + g_free(filepath); + return NULL; + } + + g_free(filepath); + + return mdb_handle_from_stream(file, flags); +} + +/** + * mdb_close: + * @mdb: Handle to open MDB database file + * + * Dereferences MDB file, closes if reference count is 0, and destroys handle. + * + **/ +void +mdb_close(MdbHandle *mdb) +{ + if (!mdb) return; + mdb_free_catalog(mdb); + g_free(mdb->stats); + g_free(mdb->backend_name); + + if (mdb->f) { + if (mdb->f->refs > 1) { + mdb->f->refs--; + } else { + if (mdb->f->stream) fclose(mdb->f->stream); + g_free(mdb->f); + } + } + + mdb_iconv_close(mdb); + mdb_remove_backends(mdb); + + g_free(mdb); +} +/** + * mdb_clone_handle: + * @mdb: Handle to open MDB database file + * + * Clones an existing database handle. Cloned handle shares the file descriptor + * but has its own page buffer, page position, and similar internal variables. + * + * Return value: new handle to the database. + */ +MdbHandle *mdb_clone_handle(MdbHandle *mdb) +{ + MdbHandle *newmdb; + MdbCatalogEntry *entry, *data; + unsigned int i; + + newmdb = (MdbHandle *) g_memdup2(mdb, sizeof(MdbHandle)); + + memset(&newmdb->catalog, 0, sizeof(MdbHandle) - offsetof(MdbHandle, catalog)); + + newmdb->catalog = g_ptr_array_new(); + for (i=0;inum_catalog;i++) { + entry = g_ptr_array_index(mdb->catalog,i); + data = g_memdup2(entry,sizeof(MdbCatalogEntry)); + data->mdb = newmdb; + data->props = NULL; + g_ptr_array_add(newmdb->catalog, data); + } + + mdb_iconv_init(newmdb); + mdb_set_default_backend(newmdb, mdb->backend_name); + + // formats for the source handle may have been changed from + // the backend's default formats, so we need to explicitly copy them here + mdb_set_date_fmt(newmdb, mdb->date_fmt); + mdb_set_shortdate_fmt(newmdb, mdb->shortdate_fmt); + mdb_set_repid_fmt(newmdb, mdb->repid_fmt); + + if (mdb->f) { + mdb->f->refs++; + } + + return newmdb; +} + +/* +** mdb_read a wrapper for read that bails if anything is wrong +*/ +ssize_t mdb_read_pg(MdbHandle *mdb, unsigned long pg) +{ + ssize_t len; + + if (pg && mdb->cur_pg == pg) return mdb->fmt->pg_size; + + len = _mdb_read_pg(mdb, mdb->pg_buf, pg); + //fprintf(stderr, "read page %ld type %02x\n", pg, mdb->pg_buf[0]); + mdb->cur_pg = pg; + /* kan - reset the cur_pos on a new page read */ + mdb->cur_pos = 0; /* kan */ + return len; +} +ssize_t mdb_read_alt_pg(MdbHandle *mdb, unsigned long pg) +{ + return _mdb_read_pg(mdb, mdb->alt_pg_buf, pg); +} +static ssize_t _mdb_read_pg(MdbHandle *mdb, void *pg_buf, unsigned long pg) +{ + ssize_t len; + off_t offset = pg * mdb->fmt->pg_size; + + if (fseeko(mdb->f->stream, 0, SEEK_END) == -1) { + fprintf(stderr, "Unable to seek to end of file\n"); + return 0; + } + if (ftello(mdb->f->stream) < offset) { + fprintf(stderr,"offset %" PRIu64 " is beyond EOF\n",(uint64_t)offset); + return 0; + } + if (mdb->stats && mdb->stats->collect) + mdb->stats->pg_reads++; + + if (fseeko(mdb->f->stream, offset, SEEK_SET) == -1) { + fprintf(stderr, "Unable to seek to page %lu\n", pg); + return 0; + } + len = fread(pg_buf, 1, mdb->fmt->pg_size, mdb->f->stream); + if (ferror(mdb->f->stream)) { + perror("read"); + return 0; + } + memset(pg_buf + len, 0, mdb->fmt->pg_size - len); + /* + * unencrypt the page if necessary. + * it might make sense to cache the unencrypted data blocks? + */ + if (pg != 0 && mdb->f->db_key != 0) + { + uint32_t tmp_key_i = mdb->f->db_key ^ pg; + unsigned char tmp_key[4] = { + tmp_key_i & 0xFF, (tmp_key_i >> 8) & 0xFF, + (tmp_key_i >> 16) & 0xFF, (tmp_key_i >> 24) & 0xFF }; + mdbi_rc4(tmp_key, sizeof(tmp_key), pg_buf, mdb->fmt->pg_size); + } + + return mdb->fmt->pg_size; +} +void mdb_swap_pgbuf(MdbHandle *mdb) +{ +char tmpbuf[MDB_PGSIZE]; + + memcpy(tmpbuf,mdb->pg_buf, MDB_PGSIZE); + memcpy(mdb->pg_buf,mdb->alt_pg_buf, MDB_PGSIZE); + memcpy(mdb->alt_pg_buf,tmpbuf,MDB_PGSIZE); +} + + +unsigned char mdb_get_byte(void *buf, int offset) +{ + return ((unsigned char *)(buf))[offset]; +} +unsigned char mdb_pg_get_byte(MdbHandle *mdb, int offset) +{ + if (offset < 0 || offset+1 > mdb->fmt->pg_size) return -1; + mdb->cur_pos++; + return mdb->pg_buf[offset]; +} + +int mdb_get_int16(void *buf, int offset) +{ + unsigned char *u8_buf = (unsigned char *)buf + offset; + return ((uint32_t)u8_buf[0] << 0) + ((uint32_t)u8_buf[1] << 8); +} +int mdb_pg_get_int16(MdbHandle *mdb, int offset) +{ + if (offset < 0 || offset+2 > mdb->fmt->pg_size) return -1; + mdb->cur_pos+=2; + return mdb_get_int16(mdb->pg_buf, offset); +} + +long mdb_get_int32_msb(void *buf, int offset) +{ + unsigned char *u8_buf = (unsigned char *)buf + offset; + return + ((uint32_t)u8_buf[0] << 24) + + ((uint32_t)u8_buf[1] << 16) + + ((uint32_t)u8_buf[2] << 8) + + ((uint32_t)u8_buf[3] << 0); +} +long mdb_get_int32(void *buf, int offset) +{ + unsigned char *u8_buf = (unsigned char *)buf + offset; + return + ((uint32_t)u8_buf[0] << 0) + + ((uint32_t)u8_buf[1] << 8) + + ((uint32_t)u8_buf[2] << 16) + + ((uint32_t)u8_buf[3] << 24); +} +long mdb_pg_get_int32(MdbHandle *mdb, int offset) +{ + if (offset <0 || offset+4 > mdb->fmt->pg_size) return -1; + mdb->cur_pos+=4; + return mdb_get_int32(mdb->pg_buf, offset); +} + +float mdb_get_single(void *buf, int offset) +{ + union {uint32_t g; float f;} f; + unsigned char *u8_buf = (unsigned char *)buf + offset; + f.g = ((uint32_t)u8_buf[0] << 0) + + ((uint32_t)u8_buf[1] << 8) + + ((uint32_t)u8_buf[2] << 16) + + ((uint32_t)u8_buf[3] << 24); + return f.f; +} +float mdb_pg_get_single(MdbHandle *mdb, int offset) +{ + if (offset <0 || offset+4 > mdb->fmt->pg_size) return -1; + mdb->cur_pos+=4; + return mdb_get_single(mdb->pg_buf, offset); +} + +double mdb_get_double(void *buf, int offset) +{ + union {uint64_t g; double d;} d; + unsigned char *u8_buf = (unsigned char *)buf + offset; + d.g = ((uint64_t)u8_buf[0] << 0) + + ((uint64_t)u8_buf[1] << 8) + + ((uint64_t)u8_buf[2] << 16) + + ((uint64_t)u8_buf[3] << 24) + + ((uint64_t)u8_buf[4] << 32) + + ((uint64_t)u8_buf[5] << 40) + + ((uint64_t)u8_buf[6] << 48) + + ((uint64_t)u8_buf[7] << 56); + return d.d; +} + +double mdb_pg_get_double(MdbHandle *mdb, int offset) +{ + if (offset <0 || offset+8 > mdb->fmt->pg_size) return -1; + mdb->cur_pos+=8; + return mdb_get_double(mdb->pg_buf, offset); +} + +int +mdb_set_pos(MdbHandle *mdb, int pos) +{ + if (pos<0 || pos >= mdb->fmt->pg_size) return 0; + + mdb->cur_pos=pos; + return pos; +} +int mdb_get_pos(MdbHandle *mdb) +{ + return mdb->cur_pos; +} diff --git a/wlx/dbview/src/libmdb/iconv.c b/wlx/dbview/src/libmdb/iconv.c new file mode 100644 index 0000000..c583c26 --- /dev/null +++ b/wlx/dbview/src/libmdb/iconv.c @@ -0,0 +1,375 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include "mdbtools.h" + +#ifndef MIN +#define MIN(a,b) (a>b ? b : a) +#endif + +static size_t decompress_unicode(const char *src, size_t slen, char *dst, size_t dlen) { + unsigned int compress=1; + size_t tlen = 0; + while (slen > 0 && tlen < dlen) { + if (*src == 0) { + compress = (compress) ? 0 : 1; + src++; + slen--; + } else if (compress) { + dst[tlen++] = *src++; + dst[tlen++] = 0; + slen--; + } else if (slen >= 2){ + dst[tlen++] = *src++; + dst[tlen++] = *src++; + slen-=2; + } else { // Odd # of bytes + break; + } + } + return tlen; +} + +#ifdef HAVE_ICONV +static size_t decompressed_to_utf8_with_iconv(MdbHandle *mdb, const char *in_ptr, size_t len_in, char *dest, size_t dlen) { + char *out_ptr = dest; + size_t len_out = dlen - 1; + + while (len_out) { + iconv(mdb->iconv_in, (ICONV_CONST char **)&in_ptr, &len_in, &out_ptr, &len_out); + /* + * Have seen database with odd number of bytes in UCS-2, shouldn't happen but protect against it + */ + if (!IS_JET3(mdb) && len_in<=1) { + //fprintf(stderr, "Detected invalid number of UCS-2 bytes\n"); + break; + } + if (!len_in || !len_out || errno == E2BIG) break; + /* Don't bail if impossible conversion is encountered */ + in_ptr += (IS_JET3(mdb)) ? 1 : 2; + len_in -= (IS_JET3(mdb)) ? 1 : 2; + *out_ptr++ = '?'; + len_out--; + } + dlen -= len_out + 1; + dest[dlen] = '\0'; + return dlen; +} +#else +static size_t latin1_to_utf8_without_iconv(const char *in_ptr, size_t len_in, char *dest, size_t dlen) { + char *out = dest; + size_t i; + for(i=0; i> 7); i++) { + unsigned char c = in_ptr[i]; + if(c & 0x80) { + *out++ = 0xC0 | (c >> 6); + *out++ = 0x80 | (c & 0x3F); + } else { + *out++ = c; + } + } + *out = '\0'; + return out - dest; +} + +static size_t unicode2ascii_locale(mdb_locale_t locale, const char *in_ptr, size_t len_in, char *dest, size_t dlen) { + size_t i; + size_t count = 0; + size_t len_out = dlen - 1; + wchar_t *w = malloc((len_in/2+1)*sizeof(wchar_t)); + + for(i=0; if->code_page == 1252) { + return latin1_to_utf8_without_iconv(in_ptr, len_in, dest, dlen); + } + int count = 0; + snprintf(dest, dlen, "%.*s%n", (int)len_in, in_ptr, &count); + return count; + } + return unicode2ascii_locale(mdb->locale, in_ptr, len_in, dest, dlen); +} +#endif + +/* + * This function is used in reading text data from an MDB table. + * 'dest' will receive a converted, null-terminated string. + * dlen is the available size of the destination buffer. + * Returns the length of the converted string, not including the terminator. + */ +int +mdb_unicode2ascii(MdbHandle *mdb, const char *src, size_t slen, char *dest, size_t dlen) +{ + char *tmp = NULL; + size_t len_in; + const char *in_ptr = NULL; + + if ((!src) || (!dest) || (!dlen)) + return 0; + + /* Uncompress 'Unicode Compressed' string into tmp */ + if (!IS_JET3(mdb) && (slen>=2) + && ((src[0]&0xff)==0xff) && ((src[1]&0xff)==0xfe)) { + tmp = g_malloc(slen*2); + len_in = decompress_unicode(src + 2, slen - 2, tmp, slen * 2); + in_ptr = tmp; + } else { + len_in = slen; + in_ptr = src; + } + +#ifdef HAVE_ICONV + dlen = decompressed_to_utf8_with_iconv(mdb, in_ptr, len_in, dest, dlen); +#else + dlen = decompressed_to_utf8_without_iconv(mdb, in_ptr, len_in, dest, dlen); +#endif + + if (tmp) g_free(tmp); + return dlen; +} + +/* + * This function is used in writing text data to an MDB table. + * If slen is 0, strlen will be used to calculate src's length. + */ +int +mdb_ascii2unicode(MdbHandle *mdb, const char *src, size_t slen, char *dest, size_t dlen) +{ + size_t len_in, len_out; + const char *in_ptr = NULL; + char *out_ptr = NULL; + + if ((!src) || (!dest) || (!dlen)) + return 0; + + in_ptr = src; + out_ptr = dest; + len_in = (slen) ? slen : strlen(in_ptr); + len_out = dlen; + +#ifdef HAVE_ICONV + iconv(mdb->iconv_out, (ICONV_CONST char **)&in_ptr, &len_in, &out_ptr, &len_out); + //printf("len_in %d len_out %d\n", len_in, len_out); + dlen -= len_out; +#else + if (IS_JET3(mdb)) { + int count; + snprintf(out_ptr, len_out, "%.*s%n", (int)len_in, in_ptr, &count); + dlen = count; + } else { + unsigned int i; + slen = MIN(len_in, len_out/2); + dlen = slen*2; + for (i=0; i4)) { + unsigned char *tmp = g_malloc(dlen); + unsigned int tptr = 0, dptr = 0; + int comp = 1; + + tmp[tptr++] = 0xff; + tmp[tptr++] = 0xfe; + while((dptr < dlen) && (tptr < dlen)) { + if (((dest[dptr+1]==0) && (comp==0)) + || ((dest[dptr+1]!=0) && (comp==1))) { + /* switch encoding mode */ + tmp[tptr++] = 0; + comp = (comp) ? 0 : 1; + } else if (dest[dptr]==0) { + /* this string cannot be compressed */ + tptr = dlen; + } else if (comp==1) { + /* encode compressed character */ + tmp[tptr++] = dest[dptr]; + dptr += 2; + } else if (tptr+1 < dlen) { + /* encode uncompressed character */ + tmp[tptr++] = dest[dptr]; + tmp[tptr++] = dest[dptr+1]; + dptr += 2; + } else { + /* could not encode uncompressed character + * into single byte */ + tptr = dlen; + } + } + if (tptr < dlen) { + memcpy(dest, tmp, tptr); + dlen = tptr; + } + g_free(tmp); + } + + return dlen; +} + +const char* +mdb_target_charset(MdbHandle *mdb) +{ +#ifdef HAVE_ICONV + const char *iconv_code = getenv("MDBICONV"); + if (!iconv_code) + iconv_code = "UTF-8"; + return iconv_code; +#else + if (!IS_JET3(mdb)) + return "ISO-8859-1"; + return NULL; // same as input: unknown +#endif +} + +/* See: https://docs.microsoft.com/en-us/windows/win32/Intl/code-page-identifiers */ +#ifdef HAVE_ICONV +static const char *mdb_iconv_name_from_code_page(int code_page) { + const char *jet3_iconv_code = NULL; + switch (code_page) { + case 437: jet3_iconv_code="IBM437"; break; + case 850: jet3_iconv_code="IBM850"; break; + case 852: jet3_iconv_code="IBM852"; break; + case 855: jet3_iconv_code="IBM855"; break; + case 860: jet3_iconv_code="IBM860"; break; + case 861: jet3_iconv_code="IBM861"; break; + case 862: jet3_iconv_code="IBM862"; break; + case 863: jet3_iconv_code="IBM863"; break; + case 864: jet3_iconv_code="IBM864"; break; + case 865: jet3_iconv_code="IBM865"; break; + case 866: jet3_iconv_code="IBM866"; break; + case 869: jet3_iconv_code="IBM869"; break; + case 874: jet3_iconv_code="WINDOWS-874"; break; + case 932: jet3_iconv_code="SHIFT-JIS"; break; + case 936: jet3_iconv_code="WINDOWS-936"; break; + case 950: jet3_iconv_code="BIG-5"; break; + case 951: jet3_iconv_code="BIG5-HKSCS"; break; + case 1200: jet3_iconv_code="UTF-16LE"; break; + case 1201: jet3_iconv_code="UTF-16BE"; break; + case 1250: jet3_iconv_code="WINDOWS-1250"; break; + case 1251: jet3_iconv_code="WINDOWS-1251"; break; + case 1252: jet3_iconv_code="WINDOWS-1252"; break; + case 1253: jet3_iconv_code="WINDOWS-1253"; break; + case 1254: jet3_iconv_code="WINDOWS-1254"; break; + case 1255: jet3_iconv_code="WINDOWS-1255"; break; + case 1256: jet3_iconv_code="WINDOWS-1256"; break; + case 1257: jet3_iconv_code="WINDOWS-1257"; break; + case 1258: jet3_iconv_code="WINDOWS-1258"; break; + case 1361: jet3_iconv_code="CP1361"; break; + case 12000: jet3_iconv_code="UTF-32LE"; break; + case 12001: jet3_iconv_code="UTF-32BE"; break; + case 20866: jet3_iconv_code="KOI8-R"; break; + case 20932: jet3_iconv_code="EUC-JP"; break; + case 21866: jet3_iconv_code="KOI8-U"; break; + case 28591: jet3_iconv_code="ISO-8859-1"; break; + case 28592: jet3_iconv_code="ISO-8859-2"; break; + case 28593: jet3_iconv_code="ISO-8859-3"; break; + case 28594: jet3_iconv_code="ISO-8859-4"; break; + case 28595: jet3_iconv_code="ISO-8859-5"; break; + case 28596: jet3_iconv_code="ISO-8859-6"; break; + case 28597: jet3_iconv_code="ISO-8859-7"; break; + case 28598: jet3_iconv_code="ISO-8859-8"; break; + case 28599: jet3_iconv_code="ISO-8859-9"; break; + case 28503: jet3_iconv_code="ISO-8859-13"; break; + case 28505: jet3_iconv_code="ISO-8859-15"; break; + case 51932: jet3_iconv_code="EUC-JP"; break; + case 51936: jet3_iconv_code="EUC-CN"; break; + case 51949: jet3_iconv_code="EUC-KR"; break; + case 65000: jet3_iconv_code="UTF-7"; break; + case 65001: jet3_iconv_code="UTF-8"; break; + default: break; + } + return jet3_iconv_code; +} +#endif + +void mdb_iconv_init(MdbHandle *mdb) +{ + const char *iconv_code; + + /* check environment variable */ + if (!(iconv_code=getenv("MDBICONV"))) { + iconv_code="UTF-8"; + } + +#ifdef HAVE_ICONV + if (!IS_JET3(mdb)) { + mdb->iconv_out = iconv_open("UCS-2LE", iconv_code); + mdb->iconv_in = iconv_open(iconv_code, "UCS-2LE"); + } else { + /* check environment variable */ + const char *jet3_iconv_code = getenv("MDB_JET3_CHARSET"); + + if (!jet3_iconv_code) { + /* Use code page embedded in the database */ + /* Note that individual columns can override this value, + * but per-column code pages are not supported by libmdb */ + jet3_iconv_code = mdb_iconv_name_from_code_page(mdb->f->code_page); + } + if (!jet3_iconv_code) { + jet3_iconv_code = "CP1252"; + } + + mdb->iconv_out = iconv_open(jet3_iconv_code, iconv_code); + mdb->iconv_in = iconv_open(iconv_code, jet3_iconv_code); + } +#elif defined(_WIN32) || defined(WIN32) || defined(_WIN64) || defined(WIN64) || defined(WINDOWS) + mdb->locale = _create_locale(LC_CTYPE, ".65001"); +#else + mdb->locale = newlocale(LC_CTYPE_MASK, "C.UTF-8", NULL); +#endif +} +void mdb_iconv_close(MdbHandle *mdb) +{ +#ifdef HAVE_ICONV + if (mdb->iconv_out != (iconv_t)-1) iconv_close(mdb->iconv_out); + if (mdb->iconv_in != (iconv_t)-1) iconv_close(mdb->iconv_in); +#elif defined(_WIN32) || defined(WIN32) || defined(_WIN64) || defined(WIN64) || defined(WINDOWS) + if (mdb->locale) _free_locale(mdb->locale); +#else + if (mdb->locale) freelocale(mdb->locale); +#endif +} + + diff --git a/wlx/dbview/src/libmdb/index.c b/wlx/dbview/src/libmdb/index.c new file mode 100644 index 0000000..d25a415 --- /dev/null +++ b/wlx/dbview/src/libmdb/index.c @@ -0,0 +1,1150 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2000-2004 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mdbtools.h" +#include "mdbprivate.h" +#ifdef HAVE_LIBMSWSTR +#include +#endif + +static MdbIndexPage *mdb_index_read_bottom_pg(MdbHandle *mdb, MdbIndex *idx, MdbIndexChain *chain); +static MdbIndexPage *mdb_chain_add_page(MdbHandle *mdb, MdbIndexChain *chain, guint32 pg); + +char idx_to_text[] = { +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0-7 0x00-0x07 */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 8-15 0x09-0x0f */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 16-23 0x10-0x17 */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 24-31 0x19-0x1f */ +' ', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 32-39 0x20-0x27 */ +0x00, 0x00, 0x00, 0x00, 0x00, ' ', ' ', 0x00, /* 40-47 0x29-0x2f */ +'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', /* 48-55 0x30-0x37 */ +'^', '_', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 56-63 0x39-0x3f */ +0x00, '`', 'a', 'b', 'd', 'f', 'g', 'h', /* 64-71 0x40-0x47 */ +'i', 'j', 'k', 'l', 'm', 'o', 'p', 'r', /* 72-79 0x49-0x4f H */ +'s', 't', 'u', 'v', 'w', 'x', 'z', '{', /* 80-87 0x50-0x57 P */ +'|', '}', '~', '5', '6', '7', '8', '9', /* 88-95 0x59-0x5f */ +0x00, '`', 'a', 'b', 'd', 'f', 'g', 'h', /* 96-103 0x60-0x67 */ +'i', 'j', 'k', 'l', 'm', 'o', 'p', 'r', /* 014-111 0x69-0x6f h */ +'s', 't', 'u', 'v', 'w', 'x', 'z', '{', /* 112-119 0x70-0x77 p */ +'|', '}', '~', 0x00, 0x00, 0x00, 0x00, 0x00, /* 120-127 0x78-0x7f */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 128-135 0x80-0x87 */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0x88-0x8f */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0x90-0x97 */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0x98-0x9f */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0xa0-0xa7 */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0xa8-0xaf */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0xb0-0xb7 */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0xb8-0xbf */ +0x00, 0x00, 0x00, 0x00, 0x00, '`', 0x00, 0x00, /* 0xc0-0xc7 */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0xc8-0xcf */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0xd0-0xd7 */ +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0xd8-0xdf */ +0x00, '`', 0x00, '`', '`', '`', 0x00, 0x00, /* 0xe0-0xe7 */ +'f', 'f', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0xe8-0xef */ +0x00, 0x00, 0x00, 'r', 0x00, 0x00, 'r', 0x00, /* 0xf0-0xf7 */ +0x81, 0x00, 0x00, 0x00, 'x', 0x00, 0x00, 0x00, /* 0xf8-0xff */ +}; + +/* This table doesn't really work accurately, as it is missing + * a lot of special processing, therefore do not use! + * This is just some kind of fallback if MSWSTR cannot be used + * for whatever reason and may not work for most indexes, i.e. + * those containing hyphens etc. + */ +char idx_to_text_ling[] = { +0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, /* 0-7 0x00-0x07 */ +0x01, 0x08, 0x08, 0x08, 0x08, 0x08, 0x01, 0x01, /* 8-15 0x08-0x0F */ +0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, /* 16-23 0x10-0x17 */ +0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, /* 24-31 0x18-0x1F */ +0x07, 0x09, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x01, /* 32-39 0x20-0x27 */ +0x14, 0x16, 0x18, ',', 0x1A, 0x01, 0x1C, 0x1E, /* 40-47 0x28-0x2F */ + '6', '8', ':', '<', '>', '@', 'B', 'D', /* 48-55 0x30-0x37 */ + 'F', 'H', ' ', '"', '.', '0', '2', '$', /* 56-63 0x38-0x3F */ + '&', 'J', 'L', 'M', 'O', 'Q', 'S', 'U', /* 64-71 0x40-0x47 */ + 'W', 'Y', '[', '\\', '^', '`', 'b', 'd', /* 72-79 0x48-0x4F */ + 'f', 'h', 'i', 'k', 'm', 'o', 'q', 's', /* 80-87 0x50-0x57 */ + 'u', 'v', 'x', '\'', ')', '*', '+', '+', /* 88-95 0x58-0x5F */ + '+', 'J', 'L', 'M', 'O', 'Q', 'S', 'U', /* 96-103 0x60-0x67 */ + 'W', 'Y', '[', '\\', '^', '`', 'b', 'd', /* 104-111 0x68-0x6F */ + 'f', 'h', 'i', 'k', 'm', 'o', 'q', 's', /* 112-119 0x70-0x77 */ + 'u', 'v', 'x', '+', '+', '+', '+', 0x01, /* 120-127 0x78-0x7F */ +0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, /* 128-135 0x80-0x87 */ +0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, /* 136-143 0x88-0x8F */ +0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, /* 144-151 0x90-0x97 */ +0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, /* 152-159 0x98-0x9F */ +0x08, '+', '4', '4', '4', '4', '+', '4', /* 160-167 0xA0-0xA7 */ + '+', '4', 'J', '3', '4', 0x01, '4', '+', /* 168-175 0xA8-0xAF */ + '4', '3', ':', '<', '+', '4', '4', '4', /* 176-183 0xB0-0xB7 */ + '+', '8', 'd', '3', '7', '7', '7', '+', /* 184-191 0xB8-0xBF */ + 'J', 'J', 'J', 'J', 'J', 'J', 'J', 'M', /* 192-199 0xC0-0xC7 */ + 'Q', 'Q', 'Q', 'Q', 'Y', 'Y', 'Y', 'Y', /* 200-207 0xC8-0xCF */ + 'O', 'b', 'd', 'd', 'd', 'd', 'd', '3', /* 208-215 0xD0-0xD7 */ + 'd', 'o', 'o', 'o', 'o', 'v', 'm', 'k', /* 216-223 0xD8-0xDF */ + 'J', 'J', 'J', 'J', 'J', 'J', 'J', 'M', /* 224-231 0xE0-0xE7 */ + 'Q', 'Q', 'Q', 'Q', 'Y', 'Y', 'Y', 'Y', /* 232-239 0xE8-0xEF */ + 'O', 'b', 'd', 'd', 'd', 'd', 'd', '3', /* 240-247 0xF0-0xF7 */ + 'd', 'o', 'o', 'o', 'o', 'v', 'm', 'v', /* 248-255 0xF8-0xFF */ +}; + +/* JET Red (v4) Index definition byte layouts + * + * Based on: + * + * http://jabakobob.net/mdb/table-page.html + * https://github.com/jahlborn/jackcess + * + * plus inspection of JET (Red) 4 databases. (JET 3 format has fewer + * fields -- some of the ones below omitted, and others narrower.) + * + * See also JET Blue (Extensible Storage Engine) format information: + * + * https://github.com/libyal/libesedb/blob/master/documentation/Extensible%20Storage%20Engine%20%28ESE%29%20Database%20File%20%28EDB%29%20format.asciidoc + * + * which is a later Microsoft embedded database format with the same + * early base format. + * + * ---------------------------------------------------------------------- + * Index Column Definitions: + * - for each "non foreign key" index (ie pidx->index_type!=2), a list + * of columns indexed + * + * Repeated table->num_real_idxs times: + * + * Offset Bytes Meaning + * 0x0000 4 UNKNOWN; seems to be type marker, usually 1923 or 0 + * + * 0x0004 2 Column 1 ID + * 0x0006 1 Column 1 Flags + * 0x0007 2 Column 2 ID + * 0x0009 1 Column 2 Flags + * 0x000A 2 Column 3 ID + * 0x000C 1 Column 3 Flags + * 0x000D 2 Column 4 ID + * 0x000F 1 Column 4 Flags + * 0x0010 2 Column 5 ID + * 0x0012 1 Column 5 Flags + * 0x0013 2 Column 6 ID + * 0x0015 1 Column 6 Flags + * 0x0016 2 Column 7 ID + * 0x0018 1 Column 7 Flags + * 0x0019 2 Column 8 ID + * 0x001B 1 Column 8 Flags + * 0x001C 2 Column 9 ID + * 0x001E 1 Column 9 Flags + * 0x001F 2 Column 10 ID + * 0x0021 1 Column 10 Flags + * + * 0x0022 1 Usage Map row + * 0x0023 3 Usage Map page (24-bit) + * 0x0026 4 First index page + * 0x002A 4 UNKNOWN + * 0x002E 2 Index Flags + * 0x0030 4 UNKNOWN; seems to always be 0 + * 0x0034 + * + * Column ID of 0xFFFF (-1) means "not used" or "end of used columns". + * Column Flags: + * - 0x01 = Ascending + * + * Index Flags: + * - 0x0001 = Unique index + * - 0x0002 = Ignore NULLs + * - 0x0008 = Required Index + * + * ---------------------------------------------------------------------- + * Index Definitions + * - for each index (normal, primary key, foreign key), details on the + * index. + * + * - this appears to be the union of information required for normal/ + * primary key indexes, and the information required for foreign key + * indexes. + * + * Repeated table->num_idxs times: + * + * Offset Bytes Meaning + * 0x0000 4 UNKNOWN; apparently a type marker, usually 1625 or 0 + * 0x0004 4 Logical Index Number + * 0x0008 4 Index Column Definition Entry + * 0x000C 1 FK Index Type + * 0x000D 4 FK Index Number + * 0x0011 4 FK Index Table Page Number + * 0x0015 1 Flags: Update Action + * 0x0016 1 Flags: Delete Action + * 0x0017 1 Index Type + * 0x0018 4 UNKNNOWN; seems to always be 0 + * 0x001B + * + * Where Index Type is: + * 0x01 = normal + * 0x01 = primary key + * 0x02 = foreign key index reference + */ + +/* Debugging helper to dump out raw hex values of index definition */ +/* +static void hexdump(unsigned char *tmpbuf, int size) { + mdb_buffer_dump(tmpbuf, 0, size); +} +*/ + + +GPtrArray * +mdb_read_indices(MdbTableDef *table) +{ + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + MdbFormatConstants *fmt = mdb->fmt; + MdbIndex *pidx; + unsigned int i, j, k; + int key_num, col_num, cleaned_col_num; + int cur_pos, name_sz, idx2_sz, type_offset; + int index_start_pg = mdb->cur_pg; + gchar *tmpbuf; + + table->indices = g_ptr_array_new(); + + if (IS_JET3(mdb)) { + cur_pos = table->index_start + 39 * table->num_real_idxs; + idx2_sz = 20; + type_offset = 19; + } else { + cur_pos = table->index_start + 52 * table->num_real_idxs; + idx2_sz = 28; + type_offset = 23; + } + + /* Read in the definitions of table indexes, into table->indices */ + + /* num_real_idxs should be the number of indexes other than type 2. + * It's not always the case. Happens on Northwind Orders table. + */ + //fprintf(stderr, "num_idxs:%d num_real_idxs:%d\n", table->num_idxs, table->num_real_idxs); + + unsigned int num_idxs_type_other_than_2 = 0; + tmpbuf = g_malloc(idx2_sz); + for (i=0;inum_idxs;i++) { + if (!read_pg_if_n(mdb, tmpbuf, &cur_pos, idx2_sz)) { + g_free(tmpbuf); + mdb_free_indices(table->indices); + return table->indices = NULL; + } + //fprintf(stderr, "Index defn: "); + //hexdump((unsigned char *)tmpbuf, idx2_sz); + pidx = g_malloc0(sizeof(MdbIndex)); + pidx->table = table; + pidx->index_num = mdb_get_int16(tmpbuf, 4); + pidx->index_type = tmpbuf[type_offset]; + g_ptr_array_add(table->indices, pidx); + /* + { + gint32 idx_marker = mdb_get_int32(tmpbuf, 0); + gint32 index_col_def_num = mdb_get_int16(tmpbuf, 8); + gint8 rel_idx_type = tmpbuf[0x0c]; + gint32 rel_idx_number = mdb_get_int32(tmpbuf, 0x0d); + gint32 rel_idx_page = mdb_get_int32(tmpbuf, 0x11); + gint8 update_action_flags = tmpbuf[0x15]; + gint8 delete_action_flags = tmpbuf[0x16]; + fprintf(stderr, "idx #%d: num2:%d num3:%d type:%d\n", i, pidx->index_num, index_col_def_num, pidx->index_type); + fprintf(stderr, "idx #%d: %d %d %d %d %d/%d\n", i, idx_marker, rel_idx_type, rel_idx_number, rel_idx_page, update_action_flags, delete_action_flags); + }*/ + if (pidx->index_type!=2) + num_idxs_type_other_than_2++; + } + if (num_idxs_type_other_than_2num_real_idxs) + table->num_real_idxs=num_idxs_type_other_than_2; + + //fprintf(stderr, "num_idxs:%d num_real_idxs:%d\n", table->num_idxs, table->num_real_idxs); + g_free(tmpbuf); + + /* Pick up the names of each index */ + for (i=0;inum_idxs;i++) { + pidx = g_ptr_array_index (table->indices, i); + if (IS_JET3(mdb)) { + name_sz=read_pg_if_8(mdb, &cur_pos); + } else { + name_sz=read_pg_if_16(mdb, &cur_pos); + } + tmpbuf = g_malloc(name_sz); + if (read_pg_if_n(mdb, tmpbuf, &cur_pos, name_sz)) + mdb_unicode2ascii(mdb, tmpbuf, name_sz, pidx->name, sizeof(pidx->name)); + g_free(tmpbuf); + //fprintf(stderr, "index %d type %d name %s\n", pidx->index_num, pidx->index_type, pidx->name); + } + + /* Pick up the column definitions for normal/primary key indexes */ + /* NOTE: Match should possibly be by index_col_def_num, rather + * than index_num; but in files encountered both seem to be the + * same (so left with index_num until a counter example is found). + */ + mdb_read_alt_pg(mdb, entry->table_pg); + mdb_read_pg(mdb, index_start_pg); + cur_pos = table->index_start; + for (i=0;inum_real_idxs;i++) { + /* Debugging print out, commented out + { + gchar *tmpbuf = (gchar *) g_malloc(0x34); + int now_pos = cur_pos; + read_pg_if_n(mdb, tmpbuf, &now_pos, 0x34); + fprintf(stderr, "Index defn: "); + hexdump((unsigned char *)tmpbuf, 0x34); + g_free(tmpbuf); + }*/ + + if (!IS_JET3(mdb)) cur_pos += 4; + /* look for index number i */ + for (j=0; jnum_idxs; ++j) { + pidx = g_ptr_array_index (table->indices, j); + if (pidx->index_type!=2 && (unsigned int)pidx->index_num==i) + break; + } + if (j==table->num_idxs) { + fprintf(stderr, "ERROR: can't find index #%d.\n", i); + continue; + } + //fprintf(stderr, "index %d #%d (%s) index_type:%d\n", i, pidx->index_num, pidx->name, pidx->index_type); + + pidx->num_rows = mdb_get_int32(mdb->alt_pg_buf, + fmt->tab_cols_start_offset + + (pidx->index_num*fmt->tab_ridx_entry_size)); + /* + fprintf(stderr, "ridx block1 i:%d data1:0x%08x data2:0x%08x\n", + i, + (unsigned int)mdb_get_int32(mdb->pg_buf, + fmt->tab_cols_start_offset + pidx->index_num * fmt->tab_ridx_entry_size), + (unsigned int)mdb_get_int32(mdb->pg_buf, + fmt->tab_cols_start_offset + pidx->index_num * fmt->tab_ridx_entry_size +4)); + fprintf(stderr, "pidx->num_rows:%d\n", pidx->num_rows);*/ + + /* Read columns in each index */ + key_num=0; + for (j=0;jnum_cols; k++) { + MdbColumn *col = g_ptr_array_index(table->columns,k); + if (col->col_num == col_num) { + cleaned_col_num = k; + break; + } + } + if (cleaned_col_num==-1) { + fprintf(stderr, "CRITICAL: can't find column with internal id %d in index %s\n", + col_num, pidx->name); + cur_pos++; + continue; + } + /* set column number to a 1 based column number and store */ + pidx->key_col_num[key_num] = cleaned_col_num + 1; + pidx->key_col_order[key_num] = + (read_pg_if_8(mdb, &cur_pos)) ? MDB_ASC : MDB_DESC; + //fprintf(stderr, "component %d using column #%d (internally %d)\n", j, cleaned_col_num, col_num); + key_num++; + } + pidx->num_keys = key_num; + + if (0) // DEBUGGING ONLY + { + gint32 usage_map = read_pg_if_32(mdb, &cur_pos); + fprintf(stderr, "pidx->unknown_pre_first_pg:0x%08x\n", usage_map); + } else { + cur_pos += 4; // Skip Usage map information + } + pidx->first_pg = read_pg_if_32(mdb, &cur_pos); + + if (!IS_JET3(mdb)) cur_pos += 4; + + pidx->flags = read_pg_if_8(mdb, &cur_pos); + //fprintf(stderr, "pidx->first_pg:%d pidx->flags:0x%02x\n", pidx->first_pg, pidx->flags); + if (!IS_JET3(mdb)) cur_pos += 5; + } + return NULL; +} +void +mdb_index_hash_text(MdbHandle *mdb, char *text, char *hash) +{ + unsigned int k, len=strlen(text); + char *transtbl=NULL; + + if (!IS_JET3(mdb)) + { +#ifdef __MSWSTR_H__ + char *out_ptr = alloca((len+1)*2); + unsigned int i; + // mdb_ascii2unicode doesn't work, we don't want unicode compression! + for (i=0; if->lang_id, 0), + LCMAP_LINGUISTIC_CASING | LCMAP_SORTKEY | NORM_IGNORECASE | NORM_IGNOREKANATYPE | NORM_IGNOREWIDTH, + (WCHAR*)out_ptr, len, (LPBYTE)hash, len*2))) + { + len++; +#endif + transtbl = idx_to_text_ling; +#ifdef __MSWSTR_H__ + } +#endif + } + else + { + transtbl = idx_to_text; + } + if (transtbl) + { + for (k=0;k %s (%d -> %d)\n", text, hash, len, k); +} +/* + * reverse the order of the column for hashing + */ +void +mdb_index_swap_n(unsigned char *src, int sz, unsigned char *dest) +{ + int i, j = 0; + + for (i = sz-1; i >= 0; i--) { + dest[j++] = src[i]; + } +} +void +mdb_index_cache_sarg(MdbColumn *col, MdbSarg *sarg, MdbSarg *idx_sarg) +{ + //guint32 cache_int; + unsigned char *c; + + switch (col->col_type) { + case MDB_TEXT: + mdb_index_hash_text(col->table->mdbidx, sarg->value.s, idx_sarg->value.s); + break; + + case MDB_LONGINT: + idx_sarg->value.i = GUINT32_SWAP_LE_BE(sarg->value.i); + //cache_int = sarg->value.i * -1; + c = (unsigned char *) &(idx_sarg->value.i); + c[0] |= 0x80; + //printf("int %08x %02x %02x %02x %02x\n", sarg->value.i, c[0], c[1], c[2], c[3]); + break; + + case MDB_INT: + break; + + default: + break; + } +} +#if 0 +int +mdb_index_test_sarg(MdbHandle *mdb, MdbColumn *col, MdbSarg *sarg, int offset, int len) +{ +char tmpbuf[256]; +int lastchar; + + switch (col->col_type) { + case MDB_BYTE: + return mdb_test_int(sarg, mdb_pg_get_byte(mdb, offset)); + break; + case MDB_INT: + return mdb_test_int(sarg, mdb_pg_get_int16(mdb, offset)); + break; + case MDB_LONGINT: + return mdb_test_int(sarg, mdb_pg_get_int32(mdb, offset)); + break; + case MDB_TEXT: + strncpy(tmpbuf, &mdb->pg_buf[offset],255); + lastchar = len > 255 ? 255 : len; + tmpbuf[lastchar]='\0'; + return mdb_test_string(sarg, tmpbuf); + default: + fprintf(stderr, "Calling mdb_test_sarg on unknown type. Add code to mdb_test_sarg() for type %d\n",col->col_type); + break; + } + return 1; +} +#endif +int +mdb_index_test_sargs(MdbHandle *mdb, MdbIndex *idx, char *buf, int len) +{ + unsigned int i, j; + MdbColumn *col; + MdbTableDef *table = idx->table; + MdbSarg *idx_sarg; + MdbSarg *sarg; + MdbField field; + MdbSargNode node; + //int c_offset = 0, + int c_len; + + //fprintf(stderr,"mdb_index_test_sargs called on "); + //mdb_buffer_dump(buf, 0, len); + //fprintf(stderr,"\n"); + for (i=0;inum_keys;i++) { + //c_offset++; /* the per column null indicator/flags */ + col=g_ptr_array_index(table->columns,idx->key_col_num[i]-1); + /* + * This will go away eventually + */ + if (col->col_type==MDB_TEXT) { + //c_len = strlen(&mdb->pg_buf[offset + c_offset]); + c_len = strlen(buf); + } else { + c_len = col->col_size; + //fprintf(stderr,"Only text types currently supported. How did we get here?\n"); + } + /* + * If we have no cached index values for this column, + * create them. + */ + if (col->num_sargs && !col->idx_sarg_cache) { + col->idx_sarg_cache = g_ptr_array_new(); + for (j=0;jnum_sargs;j++) { + sarg = g_ptr_array_index (col->sargs, j); + idx_sarg = g_memdup2(sarg,sizeof(MdbSarg)); + //printf("calling mdb_index_cache_sarg\n"); + mdb_index_cache_sarg(col, sarg, idx_sarg); + g_ptr_array_add(col->idx_sarg_cache, idx_sarg); + } + } + + for (j=0;jnum_sargs;j++) { + sarg = g_ptr_array_index (col->idx_sarg_cache, j); + /* XXX - kludge */ + node.op = sarg->op; + node.value = sarg->value; + //field.value = &mdb->pg_buf[offset + c_offset]; + field.value = buf; + field.siz = c_len; + field.is_null = FALSE; + /* In Jet 4 Index text hashes don't need to be converted from Unicode */ + if (!IS_JET3(mdb) && col->col_type == MDB_TEXT) + { + if (!mdb_test_string(&node, buf)) return 0; + } + else if (!mdb_test_sarg(mdb, col, &node, &field)) { + /* sarg didn't match, no sense going on */ + return 0; + } + } + } + return 1; +} +/* + * pack the pages bitmap + */ +int +mdb_index_pack_bitmap(MdbHandle *mdb, MdbIndexPage *ipg) +{ + int mask_bit = 0; + int mask_pos = IS_JET3(mdb)?0x16:0x1b; + int mask_byte = 0; + int elem = 0; + int len, start, i; + + start = ipg->idx_starts[elem++]; + + while (start) { + //fprintf(stdout, "elem %d is %d\n", elem, ipg->idx_starts[elem]); + len = ipg->idx_starts[elem] - start; + //fprintf(stdout, "len is %d\n", len); + for (i=0; i < len; i++) { + mask_bit++; + if (mask_bit==8) { + mask_bit=0; + mdb->pg_buf[mask_pos++] = mask_byte; + mask_byte = 0; + } + /* upon reaching the len, set the bit */ + } + mask_byte = (1 << mask_bit) | mask_byte; + //fprintf(stdout, "mask byte is %02x at %d\n", mask_byte, mask_pos); + start = ipg->idx_starts[elem++]; + } + /* flush the last byte if any */ + mdb->pg_buf[mask_pos++] = mask_byte; + /* remember to zero the rest of the bitmap */ + for (i = mask_pos; i < 0xf8; i++) { + mdb->pg_buf[mask_pos++] = 0; + } + return 0; +} +/* + * unpack the pages bitmap + */ +int +mdb_index_unpack_bitmap(MdbHandle *mdb, MdbIndexPage *ipg) +{ + int mask_bit = 0; + int mask_pos = IS_JET3(mdb)?0x16:0x1b; + int mask_byte; + int jet_start = IS_JET3(mdb)?0xf8:0x1e0; + int start = jet_start; + int elem = 0; + int len = 0; + + ipg->idx_starts[elem++]=start; + + //fprintf(stdout, "Unpacking index page %lu\n", ipg->pg); + do { + len = 0; + do { + mask_bit++; + if (mask_bit==8) { + mask_bit=0; + mask_pos++; + } + mask_byte = mdb->pg_buf[mask_pos]; + len++; + } while (mask_pos <= jet_start && !((1 << mask_bit) & mask_byte)); + //fprintf(stdout, "%d %d %d %d\n", mask_pos, mask_bit, mask_byte, len); + + start += len; + if (mask_pos < jet_start) ipg->idx_starts[elem++]=start; + + } while (mask_pos < jet_start); + + /* if we zero the next element, so we don't pick up the last pages starts*/ + ipg->idx_starts[elem]=0; + + return elem; +} +/* + * find the next entry on a page (either index or leaf). Uses state information + * stored in the MdbIndexPage across calls. + */ +int +mdb_index_find_next_on_page(MdbHandle *mdb, MdbIndexPage *ipg) +{ + if (!ipg->pg) return 0; + + /* if this page has not been unpacked to it */ + if (!ipg->idx_starts[0]){ + //fprintf(stdout, "Unpacking page %d\n", ipg->pg); + mdb_index_unpack_bitmap(mdb, ipg); + } + + + if (ipg->idx_starts[ipg->start_pos + 1]==0) return 0; + ipg->len = ipg->idx_starts[ipg->start_pos+1] - ipg->idx_starts[ipg->start_pos]; + ipg->start_pos++; + //fprintf(stdout, "Start pos %d\n", ipg->start_pos); + + return ipg->len; +} +void mdb_index_page_reset(MdbHandle *mdb, MdbIndexPage *ipg) +{ + ipg->offset = IS_JET3(mdb)?0xf8:0x1e0; /* start byte of the index entries */ + ipg->start_pos=0; + ipg->len = 0; + ipg->idx_starts[0]=0; +} +void mdb_index_page_init(MdbHandle *mdb, MdbIndexPage *ipg) +{ + memset(ipg, 0, sizeof(MdbIndexPage)); + mdb_index_page_reset(mdb, ipg); +} +/* + * find the next leaf page if any given a chain. Assumes any exhausted leaf + * pages at the end of the chain have been peeled off before the call. + */ +MdbIndexPage * +mdb_find_next_leaf(MdbHandle *mdb, MdbIndex *idx, MdbIndexChain *chain) +{ + MdbIndexPage *ipg, *newipg; + guint32 pg; + guint passed = 0; + + ipg = mdb_index_read_bottom_pg(mdb, idx, chain); + + /* + * If we are at the first page deep and it's not an index page then + * we are simply done. (there is no page to find + */ + + if (mdb->pg_buf[0]==MDB_PAGE_LEAF) { + /* Indexes can have leaves at the end that don't appear + * in the upper tree, stash the last index found so + * we can follow it at the end. */ + chain->last_leaf_found = ipg->pg; + return ipg; + } + + /* + * apply sargs here, currently we don't + */ + do { + ipg->len = 0; + //printf("finding next on pg %lu\n", ipg->pg); + if (!mdb_index_find_next_on_page(mdb, ipg)) { + //printf("find_next_on_page returned 0\n"); + return 0; + } + pg = mdb_get_int32_msb(mdb->pg_buf, ipg->offset + ipg->len - 3) >> 8; + //printf("Looking at pg %lu at %lu %d\n", pg, ipg->offset, ipg->len); + ipg->offset += ipg->len; + + /* + * add to the chain and call this function + * recursively. + */ + if (!mdb_chain_add_page(mdb, chain, pg)) + break; + newipg = mdb_find_next_leaf(mdb, idx, chain); + //printf("returning pg %lu\n",newipg->pg); + return newipg; + } while (!passed); + /* no more pages */ + return NULL; + +} +static MdbIndexPage * +mdb_chain_add_page(MdbHandle *mdb, MdbIndexChain *chain, guint32 pg) +{ + MdbIndexPage *ipg; + + chain->cur_depth++; + if (chain->cur_depth > MDB_MAX_INDEX_DEPTH) { + fprintf(stderr,"Error! maximum index depth of %d exceeded. This is probably due to a programming bug, If you are confident that your indexes really are this deep, adjust MDB_MAX_INDEX_DEPTH in mdbtools.h and recompile.\n", MDB_MAX_INDEX_DEPTH); + return NULL; + } + ipg = &(chain->pages[chain->cur_depth - 1]); + mdb_index_page_init(mdb, ipg); + ipg->pg = pg; + + return ipg; +} +/* + * returns the bottom page of the IndexChain, if IndexChain is empty it + * initializes it by reading idx->first_pg (the root page) + */ +static MdbIndexPage * +mdb_index_read_bottom_pg(MdbHandle *mdb, MdbIndex *idx, MdbIndexChain *chain) +{ + MdbIndexPage *ipg; + + /* + * if it's new use the root index page (idx->first_pg) + */ + if (!chain->cur_depth) { + ipg = &(chain->pages[0]); + mdb_index_page_init(mdb, ipg); + chain->cur_depth = 1; + ipg->pg = idx->first_pg; + if (!(ipg = mdb_find_next_leaf(mdb, idx, chain))) + return 0; + } else { + ipg = &(chain->pages[chain->cur_depth - 1]); + ipg->len = 0; + } + + mdb_read_pg(mdb, ipg->pg); + + return ipg; +} +/* + * unwind the stack and search for new leaf node + */ +MdbIndexPage * +mdb_index_unwind(MdbHandle *mdb, MdbIndex *idx, MdbIndexChain *chain) +{ + MdbIndexPage *ipg; + + //printf("page %lu finished\n",ipg->pg); + if (chain->cur_depth==1) { + //printf("cur_depth == 1 we're out\n"); + return NULL; + } + /* + * unwind the stack until we find something or reach + * the top. + */ + ipg = NULL; + while (chain->cur_depth>1 && ipg==NULL) { + //printf("chain depth %d\n", chain->cur_depth); + chain->cur_depth--; + ipg = mdb_find_next_leaf(mdb, idx, chain); + if (ipg) mdb_index_find_next_on_page(mdb, ipg); + } + if (chain->cur_depth==1) { + //printf("last leaf %lu\n", chain->last_leaf_found); + return NULL; + } + return ipg; +} +/* + * the main index function. + * caller provides an index chain which is the current traversal of index + * pages from the root page to the leaf. Initially passed as blank, + * mdb_index_find_next will store it's state information here. Each invocation + * then picks up where the last one left off, allowing us to scroll through + * the index one by one. + * + * Sargs are applied here but also need to be applied on the whole row b/c + * text columns may return false positives due to hashing and non-index + * columns with sarg values can't be tested here. + */ +int +mdb_index_find_next(MdbHandle *mdb, MdbIndex *idx, MdbIndexChain *chain, guint32 *pg, guint16 *row) +{ + MdbIndexPage *ipg; + int passed = 0; + int idx_sz; + int idx_start = 0; + unsigned short compress_bytes; + MdbColumn *col; + guint32 pg_row; + + ipg = mdb_index_read_bottom_pg(mdb, idx, chain); + + /* + * loop while the sargs don't match + */ + do { + ipg->len = 0; + /* + * if no more rows on this leaf, try to find a new leaf + */ + if (!mdb_index_find_next_on_page(mdb, ipg)) { + if (!chain->clean_up_mode) { + if (ipg->rc==1 || !(ipg = mdb_index_unwind(mdb, idx, chain))) + chain->clean_up_mode = 1; + } + if (chain->clean_up_mode) { + //fprintf(stdout,"in cleanup mode\n"); + + if (!chain->last_leaf_found) return 0; + mdb_read_pg(mdb, chain->last_leaf_found); + chain->last_leaf_found = mdb_get_int32( + mdb->pg_buf, 0x0c); + //printf("next leaf %lu\n", chain->last_leaf_found); + mdb_read_pg(mdb, chain->last_leaf_found); + /* reuse the chain for cleanup mode */ + chain->cur_depth = 1; + ipg = &chain->pages[0]; + mdb_index_page_init(mdb, ipg); + ipg->pg = chain->last_leaf_found; + //printf("next on page %d\n", + if (!mdb_index_find_next_on_page(mdb, ipg)) + return 0; + } + } + pg_row = mdb_get_int32_msb(mdb->pg_buf, ipg->offset + ipg->len - 4); + *row = pg_row & 0xff; + *pg = pg_row >> 8; + //printf("row = %d pg = %lu ipg->pg = %lu offset = %lu len = %d\n", *row, *pg, ipg->pg, ipg->offset, ipg->len); + col=g_ptr_array_index(idx->table->columns,idx->key_col_num[0]-1); + idx_sz = mdb_col_fixed_size(col); + /* handle compressed indexes, single key indexes only? */ + if (idx_sz<0) idx_sz = ipg->len - (ipg->start_pos==1?5:4); // Length from Index - the 4 trailing bytes (data page/row), Skip flags on first page + compress_bytes = mdb_get_int16(mdb->pg_buf, IS_JET3(mdb)?0x14:0x18); + if (idx->num_keys==1 && idx_sz>0 && compress_bytes > 1 && ipg->start_pos>1 /*ipg->len - 4 < idx_sz*/) { + //printf("short index found\n"); + //mdb_buffer_dump(ipg->cache_value, 0, idx_sz); + memcpy(&ipg->cache_value[compress_bytes-1], &mdb->pg_buf[ipg->offset], ipg->len); + //mdb_buffer_dump(ipg->cache_value, 0, idx_sz); + } else { + idx_start = ipg->offset + (ipg->len - 4 - idx_sz); + memcpy(ipg->cache_value, &mdb->pg_buf[idx_start], idx_sz); + } + + //idx_start = ipg->offset + (ipg->len - 4 - idx_sz); + passed = mdb_index_test_sargs(mdb, idx, (char *)(ipg->cache_value), idx_sz); + if (passed) ipg->rc=1; else if (ipg->rc) return 0; + + ipg->offset += ipg->len; + } while (!passed); + + //fprintf(stdout,"len = %d pos %d\n", ipg->len, ipg->mask_pos); + //mdb_buffer_dump(mdb->pg_buf, ipg->offset, ipg->len); + + return ipg->len; +} +/* + * XXX - FIX ME + * This function is grossly inefficient. It scans the entire index building + * an IndexChain to a specific row. We should be checking the index pages + * for matches against the indexed fields to find the proper leaf page, but + * getting it working first and then make it fast! + */ +int +mdb_index_find_row(MdbHandle *mdb, MdbIndex *idx, MdbIndexChain *chain, guint32 pg, guint16 row) +{ + MdbIndexPage *ipg; + int passed = 0; + guint32 pg_row = (pg << 8) | (row & 0xff); + guint32 datapg_row; + + ipg = mdb_index_read_bottom_pg(mdb, idx, chain); + + do { + ipg->len = 0; + /* + * if no more rows on this leaf, try to find a new leaf + */ + if (!mdb_index_find_next_on_page(mdb, ipg)) { + /* back to top? We're done */ + if (chain->cur_depth==1) + return 0; + + /* + * unwind the stack until we find something or reach + * the top. + */ + while (chain->cur_depth>1) { + chain->cur_depth--; + if (!(ipg = mdb_find_next_leaf(mdb, idx, chain))) + return 0; + mdb_index_find_next_on_page(mdb, ipg); + } + if (chain->cur_depth==1) + return 0; + } + /* test row and pg */ + datapg_row = mdb_get_int32_msb(mdb->pg_buf, ipg->offset + ipg->len - 4); + if (pg_row == datapg_row) { + passed = 1; + } + ipg->offset += ipg->len; + } while (!passed); + + /* index chain from root to leaf should now be in "chain" */ + return 1; +} + +void mdb_index_walk(MdbTableDef *table, MdbIndex *idx) +{ +/* + MdbHandle *mdb = table->entry->mdb; + int cur_pos = 0; + unsigned char marker; + MdbColumn *col; + unsigned int i; + + if (idx->num_keys!=1) return; + + mdb_read_pg(mdb, idx->first_pg); + cur_pos = 0xf8; + + for (i=0;inum_keys;i++) { + marker = mdb->pg_buf[cur_pos++]; + col=g_ptr_array_index(table->columns,idx->key_col_num[i]-1); + //printf("column %d coltype %d col_size %d (%d)\n",i,col->col_type, mdb_col_fixed_size(col), col->col_size); + } +*/ +} +void +mdb_index_dump(MdbTableDef *table, MdbIndex *idx) +{ + unsigned int i; + MdbColumn *col; + + fprintf(stdout,"index number %d\n", idx->index_num); + fprintf(stdout,"index name %s\n", idx->name); + fprintf(stdout,"index first page %d\n", idx->first_pg); + fprintf(stdout,"index rows %d\n", idx->num_rows); + if (idx->index_type==1) fprintf(stdout,"index is a primary key\n"); + for (i=0;inum_keys;i++) { + col=g_ptr_array_index(table->columns,idx->key_col_num[i]-1); + fprintf(stdout,"Column %s(%d) Sorted %s Unique: %s\n", + col->name, + idx->key_col_num[i], + idx->key_col_order[i]==MDB_ASC ? "ascending" : "descending", + idx->flags & MDB_IDX_UNIQUE ? "Yes" : "No" + ); + } + mdb_index_walk(table, idx); +} +/* + * compute_cost tries to assign a cost to a given index using the sargs + * available in this query. + * + * Indexes with no matching sargs are assigned 0 + * Unique indexes are preferred over non-uniques + * Operator preference is equal, like, isnull, others + */ +int mdb_index_compute_cost(MdbTableDef *table, MdbIndex *idx) +{ + unsigned int i; + MdbColumn *col; + MdbSarg *sarg = NULL; + int not_all_equal = 0; + + if (!idx->num_keys) return 0; + if (idx->num_keys > 1) { + for (i=0;inum_keys;i++) { + col=g_ptr_array_index(table->columns,idx->key_col_num[i]-1); + if (col->sargs) sarg = g_ptr_array_index (col->sargs, 0); + if (!sarg || sarg->op != MDB_EQUAL) not_all_equal++; + } + } + + col=g_ptr_array_index(table->columns,idx->key_col_num[0]-1); + /* + * if this is the first key column and there are no sargs, + * then this index is useless. + */ + if (!col->num_sargs) return 0; + + sarg = g_ptr_array_index (col->sargs, 0); + + /* + * a like with a wild card first is useless as a sarg */ + if ((sarg->op == MDB_LIKE || sarg->op == MDB_ILIKE) && sarg->value.s[0]=='%') + return 0; + + /* + * this needs a lot of tweaking. + */ + if (idx->flags & MDB_IDX_UNIQUE) { + if (idx->num_keys == 1) { + //printf("op is %d\n", sarg->op); + switch (sarg->op) { + case MDB_EQUAL: + return 1; break; + case MDB_LIKE: + case MDB_ILIKE: + return 4; break; + case MDB_ISNULL: + return 12; break; + default: + return 8; break; + } + } else { + switch (sarg->op) { + case MDB_EQUAL: + if (not_all_equal) return 2; + else return 1; + break; + case MDB_LIKE: + case MDB_ILIKE: + return 6; break; + case MDB_ISNULL: + return 12; break; + default: + return 9; break; + } + } + } else { + if (idx->num_keys == 1) { + switch (sarg->op) { + case MDB_EQUAL: + return 2; break; + case MDB_LIKE: + case MDB_ILIKE: + return 5; break; + case MDB_ISNULL: + return 12; break; + default: + return 10; break; + } + } else { + switch (sarg->op) { + case MDB_EQUAL: + if (not_all_equal) return 3; + else return 2; + break; + case MDB_LIKE: + case MDB_ILIKE: + return 7; break; + case MDB_ISNULL: + return 12; break; + default: + return 11; break; + } + } + } + return 0; +} +/* + * choose_index runs mdb_index_compute_cost for each available index and picks + * the best. + * + * Returns strategy to use (table scan, or index scan) + */ +MdbStrategy +mdb_choose_index(MdbTableDef *table, int *choice) +{ + unsigned int i; + MdbIndex *idx; + int cost = 0; + int least = 99; + + *choice = -1; + for (i=0;inum_idxs;i++) { + idx = g_ptr_array_index (table->indices, i); + cost = mdb_index_compute_cost(table, idx); + //printf("cost for %s is %d\n", idx->name, cost); + if (cost && cost < least) { + least = cost; + *choice = i; + } + } + /* and the winner is: *choice */ + if (least==99) return MDB_TABLE_SCAN; + return MDB_INDEX_SCAN; +} +void +mdb_index_scan_init(MdbHandle *mdb, MdbTableDef *table) +{ + int i; + + if (mdb_get_option(MDB_USE_INDEX) && mdb_choose_index(table, &i) == MDB_INDEX_SCAN) { + table->strategy = MDB_INDEX_SCAN; + table->scan_idx = g_ptr_array_index (table->indices, i); + table->chain = g_malloc0(sizeof(MdbIndexChain)); + table->mdbidx = mdb_clone_handle(mdb); + mdb_read_pg(table->mdbidx, table->scan_idx->first_pg); + //printf("best index is %s\n",table->scan_idx->name); + } + //printf("TABLE SCAN? %d\n", table->strategy); +} +void +mdb_index_scan_free(MdbTableDef *table) +{ + if (table->chain) { + g_free(table->chain); + table->chain = NULL; + } + if (table->mdbidx) { + mdb_close(table->mdbidx); + table->mdbidx = NULL; + } +} + +void mdb_free_indices(GPtrArray *indices) +{ + guint i; + + if (!indices) return; + for (i=0; ilen; i++) + g_free (g_ptr_array_index(indices, i)); + g_ptr_array_free(indices, TRUE); +} diff --git a/wlx/dbview/src/libmdb/like.c b/wlx/dbview/src/libmdb/like.c new file mode 100644 index 0000000..fe57752 --- /dev/null +++ b/wlx/dbview/src/libmdb/like.c @@ -0,0 +1,91 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include "mdbtools.h" + +/** + * + * @param s: String to search within. + * @param r: Search pattern. + * + * Tests the string @s to see if it matches the search pattern @r. In the + * search pattern, a percent sign indicates matching on any number of + * characters, and an underscore indicates matching any single character. + * + * @Returns: 1 if the string matches, 0 if the string does not match. + */ +int mdb_like_cmp(char *s, char *r) +{ + unsigned int i; + int ret; + + mdb_debug(MDB_DEBUG_LIKE, "comparing %s and %s", s, r); + switch (r[0]) { + case '\0': + return (s[0]=='\0'); + case '_': + /* skip one character */ + return mdb_like_cmp(&s[1],&r[1]); + case '%': + /* skip any number of characters */ + /* the strlen(s)+1 is important so the next call can */ + /* if there are trailing characters */ + for(i=0;i= pgnum) ? start_pg-pgnum+1 : 0; + for (; ifmt->pg_size - 4) * 8 pages. + * + * map_ind gives us the starting usage_map entry + * offset gives us a page offset into the bitmap + */ + usage_bitlen = (mdb->fmt->pg_size - 4) * 8; + max_map_pgs = (map_sz - 1) / 4; + map_ind = (start_pg + 1) / usage_bitlen; + offset = (start_pg + 1) % usage_bitlen; + + for (; map_indfmt->pg_size) { + fprintf(stderr, "Oops! didn't get a full page at %d\n", map_pg); + return -1; + } + + usage_bitmap = mdb->alt_pg_buf + 4; + for (i=offset; ientry; + MdbHandle *mdb = entry->mdb; + gint32 pgnum; + guint32 cur_pg = 0; + int free_space; + + do { + pgnum = mdb_map_find_next(mdb, + table->free_usage_map, + table->freemap_sz, cur_pg); + //printf("looking at page %d\n", pgnum); + if (!pgnum) { + /* allocate new page */ + return mdb_alloc_page(table); + } else if (pgnum==-1) { + fprintf(stderr, "Error: mdb_map_find_next_freepage error while reading maps.\n"); + return -1; + } + cur_pg = pgnum; + + mdb_read_pg(mdb, pgnum); + free_space = mdb_pg_get_freespace(mdb); + + } while (free_space < row_size); + + //printf("page %d has %d bytes left\n", pgnum, free_space); + + return pgnum; +} diff --git a/wlx/dbview/src/libmdb/money.c b/wlx/dbview/src/libmdb/money.c new file mode 100644 index 0000000..f3c6065 --- /dev/null +++ b/wlx/dbview/src/libmdb/money.c @@ -0,0 +1,156 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 1998-1999 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include "mdbtools.h" + +#define MAX_MONEY_PRECISION 20 +#define MAX_NUMERIC_PRECISION 40 +/* +** these routines are copied from the freetds project which does something +** very similiar +*/ + +static int multiply_byte(unsigned char *product, int num, unsigned char *multiplier, size_t len); +static int do_carry(unsigned char *product, size_t len); +static char *array_to_string(unsigned char *array, size_t len, int unsigned scale, int neg); + +/** + * mdb_money_to_string + * @mdb: Handle to open MDB database file + * @start: Offset of the field within the current page + * + * Returns: the allocated string that has received the value. + */ +char *mdb_money_to_string(MdbHandle *mdb, int start) +{ + const int num_bytes=8, scale=4; + int i; + int neg=0; + unsigned char multiplier[MAX_MONEY_PRECISION] = { 1 }; + unsigned char temp[MAX_MONEY_PRECISION]; + unsigned char product[MAX_MONEY_PRECISION] = { 0 }; + unsigned char bytes[num_bytes]; + + memcpy(bytes, mdb->pg_buf + start, num_bytes); + + /* Perform two's complement for negative numbers */ + if (bytes[num_bytes-1] & 0x80) { + neg = 1; + for (i=0;ipg_buf + start + 1, num_bytes); + + /* Negative bit is stored in first byte */ + if (mdb->pg_buf[start] & 0x80) neg = 1; + for (i=0;i9) { + product[j+1]+=product[j]/10; + product[j]%=10; + } + } + if (product[j]>9) { + product[j]%=10; + } + return 0; +} +static char *array_to_string(unsigned char *array, size_t len, unsigned int scale, int neg) +{ + char *s; + unsigned int top, i, j=0; + + for (top=len;(top>0) && (top-1>scale) && !array[top-1];top--); + + /* allocate enough space for all digits + minus sign + decimal point + trailing NULL byte */ + s = g_malloc(len+3); + + if (neg) + s[j++] = '-'; + + if (top == 0) { + s[j++] = '0'; + } else { + for (i=top; i>0; i--) { + if (i == scale) s[j++]='.'; + s[j++]=array[i-1]+'0'; + } + } + s[j]='\0'; + + return s; +} diff --git a/wlx/dbview/src/libmdb/options.c b/wlx/dbview/src/libmdb/options.c new file mode 100644 index 0000000..7f0a341 --- /dev/null +++ b/wlx/dbview/src/libmdb/options.c @@ -0,0 +1,92 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2004 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include "mdbtools.h" + +#define DEBUG 1 + +static TLS unsigned long opts; +static TLS int optset; + +static void load_options(void); + +void +mdb_debug(int klass, char *fmt, ...) +{ +#ifdef DEBUG + va_list ap; + + if (!optset) load_options(); + if (klass & opts) { + va_start(ap, fmt); + vfprintf (stderr,fmt, ap); + va_end(ap); + fprintf(stderr,"\n"); + } +#endif +} + +static void +load_options() +{ + char *opt; + char *s; + char *ctx; + + if (!optset && (s=getenv("MDBOPTS"))) { + opt = strtok_r(s, ":", &ctx); + while (opt) { + if (!strcmp(opt, "use_index")) { +#ifdef HAVE_LIBMSWSTR + opts |= MDB_USE_INDEX; +#else + fprintf(stderr, "The 'use_index' argument was supplied to MDBOPTS environment variable. However, this feature requires the libmswstr library, which was not found when libmdb was compiled. As a result, the 'use_index' argument will be ignored.\n\nTo enable indexes, you will need to download libmswstr from https://github.com/leecher1337/libmswstr and then recompile libmdb. Note that the 'use_index' feature is largely untested, and may have unexpected results.\n\nTo suppress this warning, run the program again after removing the 'use_index' argument from the MDBOPTS environment variable.\n"); +#endif + } + if (!strcmp(opt, "no_memo")) { + fprintf(stderr, "The 'no_memo' argument was supplied to MDBOPTS environment variable. This argument is deprecated, and has no effect.\n\nTo suppress this warning, run the program again after removing the 'no_memo' argument from the MDBOPTS environment variable.\n"); + } + if (!strcmp(opt, "debug_like")) opts |= MDB_DEBUG_LIKE; + if (!strcmp(opt, "debug_write")) opts |= MDB_DEBUG_WRITE; + if (!strcmp(opt, "debug_usage")) opts |= MDB_DEBUG_USAGE; + if (!strcmp(opt, "debug_ole")) opts |= MDB_DEBUG_OLE; + if (!strcmp(opt, "debug_row")) opts |= MDB_DEBUG_ROW; + if (!strcmp(opt, "debug_props")) opts |= MDB_DEBUG_PROPS; + if (!strcmp(opt, "debug_all")) { + opts |= MDB_DEBUG_LIKE; + opts |= MDB_DEBUG_WRITE; + opts |= MDB_DEBUG_USAGE; + opts |= MDB_DEBUG_OLE; + opts |= MDB_DEBUG_ROW; + opts |= MDB_DEBUG_PROPS; + } + opt = strtok_r(NULL,":", &ctx); + } + } + optset = 1; +} +int +mdb_get_option(unsigned long optnum) +{ + if (!optset) load_options(); + return ((opts & optnum) > 0); +} diff --git a/wlx/dbview/src/libmdb/props.c b/wlx/dbview/src/libmdb/props.c new file mode 100644 index 0000000..4f8a412 --- /dev/null +++ b/wlx/dbview/src/libmdb/props.c @@ -0,0 +1,230 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2000-2011 Brian Bruns and others + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mdbtools.h" + +static GPtrArray * +mdb_read_props_list(MdbHandle *mdb, gchar *kkd, int len) +{ + guint32 record_len; + int pos = 0; + gchar *name; + GPtrArray *names = NULL; + int i=0; + + names = g_ptr_array_new(); +#if MDB_DEBUG + mdb_buffer_dump(kkd, 0, len); +#endif + pos = 0; + while (pos < len) { + record_len = mdb_get_int16(kkd, pos); + pos += 2; + if (mdb_get_option(MDB_DEBUG_PROPS)) { + fprintf(stderr, "%02d ",i++); + mdb_buffer_dump(kkd, pos - 2, record_len + 2); + } + name = g_malloc(3*record_len + 1); /* worst case scenario is 3 bytes out per byte in */ + mdb_unicode2ascii(mdb, &kkd[pos], record_len, name, 3*record_len + 1); + + pos += record_len; + g_ptr_array_add(names, name); +#if MDB_DEBUG + printf("new len = %d\n", names->len); +#endif + } + return names; +} +static void +free_hash_entry(gpointer key, gpointer value, gpointer user_data) +{ + g_free(key); + g_free(value); +} +void +mdb_free_props(MdbProperties *props) +{ + if (!props) return; + + if (props->name) g_free(props->name); + if (props->hash) { + g_hash_table_foreach(props->hash, free_hash_entry, 0); + g_hash_table_destroy(props->hash); + } + g_free(props); +} + +static void +do_g_free(gpointer ptr, gpointer user_data) { + g_free(ptr); +} + +static void +free_names(GPtrArray *names) { + g_ptr_array_foreach(names, do_g_free, NULL); + g_ptr_array_free(names, TRUE); +} +MdbProperties * +mdb_alloc_props(void) +{ + MdbProperties *props; + + props = g_malloc0(sizeof(MdbProperties)); + + return props; +} +static MdbProperties * +mdb_read_props(MdbHandle *mdb, GPtrArray *names, gchar *kkd, int len) +{ + guint32 record_len, name_len; + int pos = 0; + guint elem; + int dtype, dsize; + gchar *name, *value; + MdbProperties *props; + int i=0; + +#if MDB_DEBUG + mdb_buffer_dump(kkd, 0, len); +#endif + pos = 0; + + record_len = mdb_get_int16(kkd, pos); + pos += 4; + name_len = mdb_get_int16(kkd, pos); + pos += 2; + props = mdb_alloc_props(); + if (name_len) { + props->name = g_malloc(3*name_len + 1); + mdb_unicode2ascii(mdb, kkd+pos, name_len, props->name, 3*name_len + 1); + mdb_debug(MDB_DEBUG_PROPS,"prop block named: %s", props->name); + } + pos += name_len; + + props->hash = g_hash_table_new(g_str_hash, g_str_equal); + + while (pos < len) { + record_len = mdb_get_int16(kkd, pos); + dtype = kkd[pos + 3]; + elem = mdb_get_int16(kkd, pos + 4); + if (elem >= names->len) + break; + dsize = mdb_get_int16(kkd, pos + 6); + if (dsize < 0 || pos + 8 + dsize > len) + break; + value = g_strdup_printf("%.*s", dsize, &kkd[pos+8]); + name = g_ptr_array_index(names,elem); + if (mdb_get_option(MDB_DEBUG_PROPS)) { + fprintf(stderr, "%02d ",i++); + mdb_debug(MDB_DEBUG_PROPS,"elem %d (%s) dsize %d dtype %d", elem, name, dsize, dtype); + mdb_buffer_dump(value, 0, dsize); + } + if (dtype == MDB_MEMO) { + dtype = MDB_TEXT; + } else if (dtype == MDB_BINARY && dsize == 16 && strcmp(name, "GUID") == 0) { + dtype = MDB_REPID; + } + if (dtype == MDB_BOOL) { + g_hash_table_insert(props->hash, g_strdup(name), + g_strdup(kkd[pos + 8] ? "yes" : "no")); + } else if (dtype == MDB_BINARY || dtype == MDB_OLE) { + g_hash_table_insert(props->hash, g_strdup(name), + g_strdup_printf("(binary data of length %d)", dsize)); + } else { + g_hash_table_insert(props->hash, g_strdup(name), + mdb_col_to_string(mdb, kkd, pos + 8, dtype, dsize)); + } + g_free(value); + pos += record_len; + } + return props; + +} + +static void +print_keyvalue(gpointer key, gpointer value, gpointer outfile) +{ + fprintf((FILE*)outfile,"\t%s: %s\n", (gchar *)key, (gchar *)value); +} +void +mdb_dump_props(MdbProperties *props, FILE *outfile, int show_name) { + if (show_name) + fprintf(outfile,"name: %s\n", props->name ? props->name : "(none)"); + g_hash_table_foreach(props->hash, print_keyvalue, outfile); + if (show_name) + fputc('\n', outfile); +} + +/* + * That function takes a raw KKD/MR2 binary buffer, + * typically read from LvProp in table MSysbjects + * and returns a GPtrArray of MdbProps* + */ +GPtrArray* +mdb_kkd_to_props(MdbHandle *mdb, void *buffer, size_t len) { + guint32 record_len; + guint16 record_type; + size_t pos; + GPtrArray *names = NULL; + MdbProperties *props; + GPtrArray *result; + +#if MDB_DEBUG + mdb_buffer_dump(buffer, 0, len); +#endif + mdb_debug(MDB_DEBUG_PROPS,"starting prop parsing of type %s", buffer); + + if (strcmp("KKD", buffer) && strcmp("MR2", buffer)) { + fprintf(stderr, "Unrecognized format.\n"); + mdb_buffer_dump(buffer, 0, len); + return NULL; + } + + result = g_ptr_array_new(); + + pos = 4; + while (pos < len) { + record_len = mdb_get_int32(buffer, pos); + record_type = mdb_get_int16(buffer, pos + 4); + mdb_debug(MDB_DEBUG_PROPS,"prop chunk type:0x%04x len:%d", record_type, record_len); + //mdb_buffer_dump(buffer, pos+4, record_len); + switch (record_type) { + case 0x80: + if (names) free_names(names); + names = mdb_read_props_list(mdb, (char*)buffer+pos+6, record_len - 6); + break; + case 0x00: + case 0x01: + case 0x02: + if (!names) { + fprintf(stderr,"sequence error!\n"); + break; + } + props = mdb_read_props(mdb, names, (char*)buffer+pos+6, record_len - 6); + g_ptr_array_add(result, props); + //mdb_dump_props(props, stderr, 1); + break; + default: + fprintf(stderr,"Unknown record type %d\n", record_type); + break; + } + pos += record_len; + } + if (names) free_names(names); + return result; +} diff --git a/wlx/dbview/src/libmdb/rc4.c b/wlx/dbview/src/libmdb/rc4.c new file mode 100644 index 0000000..5596736 --- /dev/null +++ b/wlx/dbview/src/libmdb/rc4.c @@ -0,0 +1,85 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mdbprivate.h" + +typedef struct +{ + unsigned char state[256]; + unsigned char x; + unsigned char y; +} RC4_KEY; + +#define swap_byte(x,y) t = *(x); *(x) = *(y); *(y) = t + + +static void RC4_set_key(RC4_KEY *key, int key_data_len, unsigned char *key_data_ptr) +{ + unsigned char t; + unsigned char index1; + unsigned char index2; + unsigned char* state; + short counter; + + state = &key->state[0]; + for(counter = 0; counter < 256; counter++) + state[counter] = counter; + key->x = 0; + key->y = 0; + index1 = 0; + index2 = 0; + for(counter = 0; counter < 256; counter++) { + index2 = (key_data_ptr[index1] + state[counter] + index2) % 256; + swap_byte(&state[counter], &state[index2]); + index1 = (index1 + 1) % key_data_len; + } +} + +/* + * this algorithm does 'encrypt in place' instead of inbuff/outbuff + * note also: encryption and decryption use same routine + * implementation supplied by (Adam Back) at + */ +static void RC4(RC4_KEY *key, int buffer_len, unsigned char * buff) +{ + unsigned char t; + unsigned char x; + unsigned char y; + unsigned char* state; + unsigned char xorIndex; + short counter; + + x = key->x; + y = key->y; + state = &key->state[0]; + for(counter = 0; counter < buffer_len; counter++) { + x = (x + 1) % 256; + y = (state[x] + y) % 256; + swap_byte(&state[x], &state[y]); + xorIndex = (state[x] + state[y]) % 256; + buff[counter] ^= state[xorIndex]; + } + key->x = x; + key->y = y; +} + +void mdbi_rc4(unsigned char *key, guint32 key_len, unsigned char *buf, guint32 buf_len) { + RC4_KEY rc4_key; + RC4_set_key(&rc4_key, key_len, key); + RC4(&rc4_key, buf_len, buf); +} \ No newline at end of file diff --git a/wlx/dbview/src/libmdb/sargs.c b/wlx/dbview/src/libmdb/sargs.c new file mode 100644 index 0000000..d8c3c9c --- /dev/null +++ b/wlx/dbview/src/libmdb/sargs.c @@ -0,0 +1,375 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* + * code for handling searchable arguments (sargs) used primary by the sql + * engine to support where clause handling. The sargs are configured in + * a tree with AND/OR operators connecting the child nodes. NOT operations + * have only one child on the left side. Logical operators (=,<,>,etc..) + * have no children. + * + * datatype support is a bit weak at this point. To add more types create + * a mdb_test_[type]() function and invoke it from mdb_test_sarg() + */ + +#include +#include "mdbtools.h" +#include "mdbprivate.h" + +void +mdb_sql_walk_tree(MdbSargNode *node, MdbSargTreeFunc func, gpointer data) +{ + if (func(node, data)) + return; + if (node->left) mdb_sql_walk_tree(node->left, func, data); + if (node->right) mdb_sql_walk_tree(node->right, func, data); +} +int +mdb_test_string(MdbSargNode *node, char *s) +{ +int rc; + + if (node->op == MDB_LIKE) { + return mdb_like_cmp(s,node->value.s); + } + if (node->op == MDB_ILIKE) { + return mdb_ilike_cmp(s,node->value.s); + } + rc = strcoll(node->value.s, s); + switch (node->op) { + case MDB_EQUAL: + if (rc==0) return 1; + break; + case MDB_GT: + if (rc<0) return 1; + break; + case MDB_LT: + if (rc>0) return 1; + break; + case MDB_GTEQ: + if (rc<=0) return 1; + break; + case MDB_LTEQ: + if (rc>=0) return 1; + break; + case MDB_NEQ: + if (rc!=0) return 1; + break; + default: + fprintf(stderr, "Calling mdb_test_sarg on unknown operator. Add code to mdb_test_string() for operator %d\n",node->op); + break; + } + return 0; +} +int mdb_test_int(MdbSargNode *node, gint32 i) +{ + gint32 val = node->val_type == MDB_INT ? node->value.i : node->value.d; + switch (node->op) { + case MDB_EQUAL: + //fprintf(stderr, "comparing %ld and %ld\n", i, node->value.i); + if (val == i) return 1; + break; + case MDB_GT: + if (val < i) return 1; + break; + case MDB_LT: + if (val > i) return 1; + break; + case MDB_GTEQ: + if (val <= i) return 1; + break; + case MDB_LTEQ: + if (val >= i) return 1; + break; + case MDB_NEQ: + if (val != i) return 1; + break; + default: + fprintf(stderr, "Calling mdb_test_sarg on unknown operator. Add code to mdb_test_int() for operator %d\n",node->op); + break; + } + return 0; +} + +/* Actually used to not rely on libm. + * Maybe there is a cleaner and faster solution? */ +static double poor_mans_trunc(double x) +{ + char buf[16]; + snprintf(buf, sizeof(buf), "%.6f", x); + sscanf(buf, "%lf", &x); + return x; +} + +int mdb_test_double(int op, double vd, double d) +{ + int ret = 0; + switch (op) { + case MDB_EQUAL: + //fprintf(stderr, "comparing %lf and %lf\n", d, node->value.d); + ret = (vd == d); + break; + case MDB_GT: + ret = (vd < d); + break; + case MDB_LT: + ret = (vd > d); + break; + case MDB_GTEQ: + ret = (vd <= d); + break; + case MDB_LTEQ: + ret = (vd >= d); + break; + case MDB_NEQ: + ret = (vd != d); + break; + default: + fprintf(stderr, "Calling mdb_test_sarg on unknown operator. Add code to mdb_test_double() for operator %d\n",op); + break; + } + return ret; +} + +#if 0 // Obsolete +int +mdb_test_date(MdbSargNode *node, double td) +{ + struct tm found; + /* TODO: you should figure out a way to pull mdb_date_to_string in here + * char date_tmp[MDB_BIND_SIZE]; + */ + + time_t found_t; + time_t asked_t; + + double diff; + + mdb_date_to_tm(td, &found); + + asked_t = node->value.i; + found_t = mktime(&found); + + diff = difftime(asked_t, found_t); + + switch (node->op) { + case MDB_EQUAL: + if (diff==0) return 1; + break; + case MDB_GT: + if (diff<0) return 1; + break; + case MDB_LT: + if (diff>0) return 1; + break; + case MDB_GTEQ: + if (diff<=0) return 1; + break; + case MDB_LTEQ: + if (diff>=0) return 1; + break; + default: + fprintf(stderr, "Calling mdb_test_sarg on unknown operator. Add code to mdb_test_date() for operator %d\n", node->op); + break; + } + return 0; +} +#endif + + +int +mdb_find_indexable_sargs(MdbSargNode *node, gpointer data) +{ + MdbSarg sarg; + + if (node->op == MDB_OR || node->op == MDB_NOT) return 1; + + /* + * right now all we do is look for sargs that are anded together from + * the root. Later we may put together OR ops into a range, and then + * range scan the leaf pages. That is col1 = 2 or col1 = 4 becomes + * col1 >= 2 and col1 <= 4 for the purpose of index scans, and then + * extra rows are thrown out when the row is tested against the main + * sarg tree. range scans are generally only a bit better than table + * scanning anyway. + * + * also, later we should support the NOT operator, but it's generally + * a pretty worthless test for indexes, ie NOT col1 = 3, we are + * probably better off table scanning. + */ + if (mdb_is_relational_op(node->op) && node->col) { + //printf("op = %d value = %s\n", node->op, node->value.s); + sarg.op = node->op; + sarg.value = node->value; + mdb_add_sarg(node->col, &sarg); + } + return 0; +} +int +mdb_test_sarg(MdbHandle *mdb, MdbColumn *col, MdbSargNode *node, MdbField *field) +{ + char tmpbuf[256]; + char* val; + int ret = 1; + + if (node->op == MDB_ISNULL) + ret = field->is_null; + else if (node->op == MDB_NOTNULL) + ret = !field->is_null; + switch (col->col_type) { + case MDB_BOOL: + ret = mdb_test_int(node, !field->is_null); + break; + case MDB_BYTE: + ret = mdb_test_int(node, (gint32)((char *)field->value)[0]); + break; + case MDB_INT: + ret = mdb_test_int(node, (gint32)mdb_get_int16(field->value, 0)); + break; + case MDB_LONGINT: + ret = mdb_test_int(node, (gint32)mdb_get_int32(field->value, 0)); + break; + case MDB_FLOAT: + ret = mdb_test_double(node->op, node->val_type == MDB_INT ? node->value.i : node->value.d, mdb_get_single(field->value, 0)); + break; + case MDB_DOUBLE: + ret = mdb_test_double(node->op, node->val_type == MDB_INT ? node->value.i : node->value.d, mdb_get_double(field->value, 0)); + break; + case MDB_TEXT: + mdb_unicode2ascii(mdb, field->value, field->siz, tmpbuf, sizeof(tmpbuf)); + ret = mdb_test_string(node, tmpbuf); + break; + case MDB_MEMO: + case MDB_REPID: + val = mdb_col_to_string(mdb, mdb->pg_buf, field->start, col->col_type, (gint32)mdb_get_int32(field->value, 0)); + //printf("%s\n",val); + ret = mdb_test_string(node, val); + g_free(val); + break; + case MDB_DATETIME: + ret = mdb_test_double(node->op, poor_mans_trunc(node->value.d), poor_mans_trunc(mdb_get_double(field->value, 0))); + break; + default: + fprintf(stderr, "Calling mdb_test_sarg on unknown type. Add code to mdb_test_sarg() for type %d\n",col->col_type); + break; + } + return ret; +} +int +mdb_find_field(int col_num, MdbField *fields, int num_fields) +{ + int i; + + for (i=0;iop)) { + col = node->col; + /* for const = const expressions */ + if (!col) { + return (node->value.i); + } + elem = mdb_find_field(col->col_num, fields, num_fields); + if (!mdb_test_sarg(mdb, col, node, &fields[elem])) + return 0; + } else { /* logical op */ + switch (node->op) { + case MDB_NOT: + rc = mdb_test_sarg_node(mdb, node->left, fields, num_fields); + return !rc; + break; + case MDB_AND: + if (!mdb_test_sarg_node(mdb, node->left, fields, num_fields)) + return 0; + return mdb_test_sarg_node(mdb, node->right, fields, num_fields); + break; + case MDB_OR: + if (mdb_test_sarg_node(mdb, node->left, fields, num_fields)) + return 1; + return mdb_test_sarg_node(mdb, node->right, fields, num_fields); + break; + } + } + return 1; +} +int +mdb_test_sargs(MdbTableDef *table, MdbField *fields, int num_fields) +{ + MdbSargNode *node; + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + + node = table->sarg_tree; + + /* there may not be a sarg tree */ + if (!node) return 1; + + return mdb_test_sarg_node(mdb, node, fields, num_fields); +} +#if 0 +int mdb_test_sargs(MdbHandle *mdb, MdbColumn *col, int offset, int len) +{ +MdbSarg *sarg; +int i; + + for (i=0;inum_sargs;i++) { + sarg = g_ptr_array_index (col->sargs, i); + if (!mdb_test_sarg(mdb, col, sarg, offset, len)) { + /* sarg didn't match, no sense going on */ + return 0; + } + } + + return 1; +} +#endif +int mdb_add_sarg(MdbColumn *col, MdbSarg *in_sarg) +{ +MdbSarg *sarg; + if (!col->sargs) { + col->sargs = g_ptr_array_new(); + } + sarg = g_memdup2(in_sarg,sizeof(MdbSarg)); + g_ptr_array_add(col->sargs, sarg); + col->num_sargs++; + + return 1; +} +int mdb_add_sarg_by_name(MdbTableDef *table, char *colname, MdbSarg *in_sarg) +{ + MdbColumn *col; + unsigned int i; + + for (i=0;inum_cols;i++) { + col = g_ptr_array_index (table->columns, i); + if (!g_ascii_strcasecmp(col->name,colname)) { + return mdb_add_sarg(col, in_sarg); + } + } + /* else didn't find the column return 0! */ + return 0; +} diff --git a/wlx/dbview/src/libmdb/stats.c b/wlx/dbview/src/libmdb/stats.c new file mode 100644 index 0000000..f60280d --- /dev/null +++ b/wlx/dbview/src/libmdb/stats.c @@ -0,0 +1,67 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mdbtools.h" + +/** + * @brief Begins collection of statistics on an MDBHandle. + * @param mdb: Handle to the (open) MDB file to collect stats on. + * + * + * Statistics in LibMDB will track the number of reads from the MDB file. The + * collection of statistics is started and stopped with the mdb_stats_on and + * mdb_stats_off functions. Collected statistics are accessed by reading the + * MdbStatistics structure or calling mdb_dump_stats. + * + */ +void +mdb_stats_on(MdbHandle *mdb) +{ + if (!mdb->stats) + mdb->stats = g_malloc0(sizeof(MdbStatistics)); + + mdb->stats->collect = TRUE; +} +/** + * @brief Turns off statistics collection. + * @param mdb: pointer to handle of MDB file with active stats collection. + * + * + * If mdb_stats_off() is not called, statistics will be turned off when handle + * is freed using mdb_close(). + **/ +void +mdb_stats_off(MdbHandle *mdb) +{ + if (!mdb->stats) return; + + mdb->stats->collect = FALSE; +} +/** + * @brief Dumps current statistics to stdout. + * @param mdb: pointer to handle of MDB file with active stats collection. + * + * + **/ +void +mdb_dump_stats(MdbHandle *mdb) +{ + if (!mdb->stats) return; + + fprintf(stdout, "Physical Page Reads: %lu\n", mdb->stats->pg_reads); +} diff --git a/wlx/dbview/src/libmdb/table.c b/wlx/dbview/src/libmdb/table.c new file mode 100644 index 0000000..1a758e4 --- /dev/null +++ b/wlx/dbview/src/libmdb/table.c @@ -0,0 +1,448 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mdbtools.h" +#include "mdbprivate.h" + +static gint mdb_col_comparer(MdbColumn **a, MdbColumn **b) +{ + if ((*a)->col_num > (*b)->col_num) + return 1; + else if ((*a)->col_num < (*b)->col_num) + return -1; + else + return 0; +} + +MdbTableDef *mdb_alloc_tabledef(MdbCatalogEntry *entry) +{ + MdbTableDef *table = g_malloc0(sizeof(MdbTableDef)); + table->entry=entry; + snprintf(table->name, sizeof(table->name), "%s", entry->object_name); + + return table; +} +void mdb_free_tabledef(MdbTableDef *table) +{ + if (!table) return; + if (table->is_temp_table) { + guint i; + /* Temp table pages are being stored in memory */ + for (i=0; itemp_table_pages->len; i++) + g_free(g_ptr_array_index(table->temp_table_pages,i)); + g_ptr_array_free(table->temp_table_pages, TRUE); + /* Temp tables use dummy entries */ + g_free(table->entry); + } + mdb_free_columns(table->columns); + mdb_free_indices(table->indices); + g_free(table->usage_map); + g_free(table->free_usage_map); + g_free(table); +} +MdbTableDef *mdb_read_table(MdbCatalogEntry *entry) +{ + MdbTableDef *table; + MdbHandle *mdb = entry->mdb; + MdbFormatConstants *fmt = mdb->fmt; + int row_start, pg_row; + void *buf, *pg_buf = mdb->pg_buf; + guint i; + + if (!mdb_read_pg(mdb, entry->table_pg)) { + fprintf(stderr, "mdb_read_table: Unable to read page %lu\n", entry->table_pg); + return NULL; + } + if (mdb_get_byte(pg_buf, 0) != 0x02) { + fprintf(stderr, "mdb_read_table: Page %lu [size=%d] is not a valid table definition page (First byte = 0x%02X, expected 0x02)\n", + entry->table_pg, (int)fmt->pg_size, mdb_get_byte(pg_buf, 0)); + return NULL; + } + table = mdb_alloc_tabledef(entry); + + mdb_get_int16(pg_buf, 8); /* len */ + + /* Note that num_rows may be zero if the database was improperly closed. + * See https://github.com/mdbtools/mdbtools/issues/120 for discussion. */ + table->num_rows = mdb_get_int32(pg_buf, fmt->tab_num_rows_offset); + table->num_var_cols = mdb_get_int16(pg_buf, fmt->tab_num_cols_offset-2); + table->num_cols = mdb_get_int16(pg_buf, fmt->tab_num_cols_offset); + table->num_idxs = mdb_get_int32(pg_buf, fmt->tab_num_idxs_offset); + table->num_real_idxs = mdb_get_int32(pg_buf, fmt->tab_num_ridxs_offset); + + /* grab a copy of the usage map */ + pg_row = mdb_get_int32(pg_buf, fmt->tab_usage_map_offset); + if (mdb_find_pg_row(mdb, pg_row, &buf, &row_start, &(table->map_sz))) { + fprintf(stderr, "mdb_read_table: Unable to find page row %d\n", pg_row); + mdb_free_tabledef(table); + return NULL; + } + /* First byte of usage_map is the map-type and must always be present */ + if (table->map_sz < 1) { + fprintf(stderr, "mdb_read_table: invalid map-size: %zu\n", table->map_sz); + mdb_free_tabledef(table); + return NULL; + } + table->usage_map = g_memdup2((char*)buf + row_start, table->map_sz); + if (mdb_get_option(MDB_DEBUG_USAGE)) + mdb_buffer_dump(buf, row_start, table->map_sz); + mdb_debug(MDB_DEBUG_USAGE,"usage map found on page %ld row %d start %d len %d", + pg_row >> 8, pg_row & 0xff, row_start, table->map_sz); + + /* grab a copy of the free space page map */ + pg_row = mdb_get_int32(pg_buf, fmt->tab_free_map_offset); + if (mdb_find_pg_row(mdb, pg_row, &buf, &row_start, &(table->freemap_sz))) { + fprintf(stderr, "mdb_read_table: Unable to find page row %d\n", pg_row); + mdb_free_tabledef(table); + return NULL; + } + table->free_usage_map = g_memdup2((char*)buf + row_start, table->freemap_sz); + mdb_debug(MDB_DEBUG_USAGE,"free map found on page %ld row %d start %d len %d\n", + pg_row >> 8, pg_row & 0xff, row_start, table->freemap_sz); + + table->first_data_pg = mdb_get_int16(pg_buf, fmt->tab_first_dpg_offset); + + if (entry->props) + for (i=0; iprops->len; ++i) { + MdbProperties *props = g_ptr_array_index(entry->props, i); + if (!props->name) + table->props = props; + } + + return table; +} +MdbTableDef *mdb_read_table_by_name(MdbHandle *mdb, gchar *table_name, int obj_type) +{ + unsigned int i; + MdbCatalogEntry *entry; + + mdb_read_catalog(mdb, obj_type); + + for (i=0; inum_catalog; i++) { + entry = g_ptr_array_index(mdb->catalog, i); + if (!g_ascii_strcasecmp(entry->object_name, table_name)) + return mdb_read_table(entry); + } + + return NULL; +} + + +guint32 +read_pg_if_32(MdbHandle *mdb, int *cur_pos) +{ + char c[4]; + + read_pg_if_n(mdb, c, cur_pos, 4); + return mdb_get_int32(c, 0); +} +guint16 +read_pg_if_16(MdbHandle *mdb, int *cur_pos) +{ + char c[2]; + + read_pg_if_n(mdb, c, cur_pos, 2); + return mdb_get_int16(c, 0); +} +guint8 +read_pg_if_8(MdbHandle *mdb, int *cur_pos) +{ + guint8 c; + + read_pg_if_n(mdb, &c, cur_pos, 1); + return c; +} +/* + * Read data into a buffer, advancing pages and setting the + * page cursor as needed. In the case that buf in NULL, pages + * are still advanced and the page cursor is still updated. + */ +void * +read_pg_if_n(MdbHandle *mdb, void *buf, int *cur_pos, size_t len) +{ + char* _buf = buf; + char* _end = buf ? buf + len : NULL; + + if (*cur_pos < 0) + return NULL; + + /* Advance to page which contains the first byte */ + while (*cur_pos >= mdb->fmt->pg_size) { + if (!mdb_read_pg(mdb, mdb_get_int32(mdb->pg_buf,4))) + return NULL; + *cur_pos -= (mdb->fmt->pg_size - 8); + } + /* Copy pages into buffer */ + while (*cur_pos + len >= (size_t)mdb->fmt->pg_size) { + size_t piece_len = mdb->fmt->pg_size - *cur_pos; + if (_buf) { + if (_buf + piece_len > _end) + return NULL; + memcpy(_buf, mdb->pg_buf + *cur_pos, piece_len); + _buf += piece_len; + } + len -= piece_len; + if (!mdb_read_pg(mdb, mdb_get_int32(mdb->pg_buf,4))) + return NULL; + *cur_pos = 8; + } + /* Copy into buffer from final page */ + if (len && _buf) { + if (_buf + len > _end) + return NULL; + memcpy(_buf, mdb->pg_buf + *cur_pos, len); + } + *cur_pos += len; + return _buf; +} + + +void mdb_append_column(GPtrArray *columns, MdbColumn *in_col) +{ + g_ptr_array_add(columns, g_memdup2(in_col,sizeof(MdbColumn))); +} +void mdb_free_columns(GPtrArray *columns) +{ + guint i, j; + MdbColumn *col; + + if (!columns) return; + for (i=0; ilen; i++) { + col = (MdbColumn *) g_ptr_array_index(columns, i); + if (col->sargs) { + for (j=0; jsargs->len; j++) { + g_free( g_ptr_array_index(col->sargs, j)); + } + g_ptr_array_free(col->sargs, TRUE); + } + g_free(col); + } + g_ptr_array_free(columns, TRUE); +} +GPtrArray *mdb_read_columns(MdbTableDef *table) +{ + MdbHandle *mdb = table->entry->mdb; + MdbFormatConstants *fmt = mdb->fmt; + MdbColumn *pcol; + unsigned char *col; + unsigned int i; + guint j; + int cur_pos; + size_t name_sz; + GPtrArray *allprops; + + table->columns = g_ptr_array_new(); + + col = g_malloc(fmt->tab_col_entry_size); + + cur_pos = fmt->tab_cols_start_offset + + (table->num_real_idxs * fmt->tab_ridx_entry_size); + + /* new code based on patch submitted by Tim Nelson 2000.09.27 */ + + /* + ** column attributes + */ + for (i=0;inum_cols;i++) { +#ifdef MDB_DEBUG + /* printf("column %d\n", i); + mdb_buffer_dump(mdb->pg_buf, cur_pos, fmt->tab_col_entry_size); */ +#endif + if (!read_pg_if_n(mdb, col, &cur_pos, fmt->tab_col_entry_size)) { + g_free(col); + mdb_free_columns(table->columns); + return table->columns = NULL; + } + pcol = g_malloc0(sizeof(MdbColumn)); + + pcol->table = table; + + pcol->col_type = col[0]; + + // col_num_offset == 1 or 5 + pcol->col_num = col[fmt->col_num_offset]; + + //fprintf(stdout,"----- column %d -----\n",pcol->col_num); + // col_var == 3 or 7 + pcol->var_col_num = mdb_get_int16(col, fmt->tab_col_offset_var); + //fprintf(stdout,"var column pos %d\n",pcol->var_col_num); + + // col_var == 5 or 9 + pcol->row_col_num = mdb_get_int16(col, fmt->tab_row_col_num_offset); + //fprintf(stdout,"row column num %d\n",pcol->row_col_num); + + if (pcol->col_type == MDB_NUMERIC || pcol->col_type == MDB_MONEY || + pcol->col_type == MDB_FLOAT || pcol->col_type == MDB_DOUBLE) { + pcol->col_scale = col[fmt->col_scale_offset]; + pcol->col_prec = col[fmt->col_prec_offset]; + } + + // col_flags_offset == 13 or 15 + pcol->is_fixed = col[fmt->col_flags_offset] & 0x01 ? 1 : 0; + pcol->is_long_auto = col[fmt->col_flags_offset] & 0x04 ? 1 : 0; + pcol->is_uuid_auto = col[fmt->col_flags_offset] & 0x40 ? 1 : 0; + + // tab_col_offset_fixed == 14 or 21 + pcol->fixed_offset = mdb_get_int16(col, fmt->tab_col_offset_fixed); + //fprintf(stdout,"fixed column offset %d\n",pcol->fixed_offset); + //fprintf(stdout,"col type %s\n",pcol->is_fixed ? "fixed" : "variable"); + + if (pcol->col_type != MDB_BOOL) { + // col_size_offset == 16 or 23 + pcol->col_size = mdb_get_int16(col, fmt->col_size_offset); + } else { + pcol->col_size=0; + } + + g_ptr_array_add(table->columns, pcol); + } + + g_free (col); + + /* + ** column names - ordered the same as the column attributes table + */ + for (i=0;inum_cols;i++) { + char *tmp_buf; + pcol = g_ptr_array_index(table->columns, i); + + if (IS_JET3(mdb)) + name_sz = read_pg_if_8(mdb, &cur_pos); + else + name_sz = read_pg_if_16(mdb, &cur_pos); + tmp_buf = g_malloc(name_sz); + if (read_pg_if_n(mdb, tmp_buf, &cur_pos, name_sz)) + mdb_unicode2ascii(mdb, tmp_buf, name_sz, pcol->name, sizeof(pcol->name)); + g_free(tmp_buf); + } + + /* Sort the columns by col_num */ + g_ptr_array_sort(table->columns, (GCompareFunc)mdb_col_comparer); + + allprops = table->entry->props; + if (allprops) + for (i=0;inum_cols;i++) { + pcol = g_ptr_array_index(table->columns, i); + for (j=0; jlen; ++j) { + MdbProperties *props = g_ptr_array_index(allprops, j); + if (props->name && !strcmp(props->name, pcol->name)) { + pcol->props = props; + break; + } + + } + } + table->index_start = cur_pos; + return table->columns; +} + +void mdb_table_dump(MdbCatalogEntry *entry) +{ +MdbTableDef *table; +MdbColumn *col; +int coln; +MdbIndex *idx; +unsigned int i, bitn; +guint32 pgnum; + + table = mdb_read_table(entry); + if (!table) + return; + + fprintf(stdout,"definition page = %lu\n",entry->table_pg); + fprintf(stdout,"number of datarows = %d\n",table->num_rows); + fprintf(stdout,"number of columns = %d\n",table->num_cols); + fprintf(stdout,"number of indices = %d\n",table->num_real_idxs); + + if (table->props) + mdb_dump_props(table->props, stdout, 0); + mdb_read_columns(table); + mdb_read_indices(table); + + for (i=0;inum_cols;i++) { + col = g_ptr_array_index(table->columns,i); + + fprintf(stdout,"column %d Name: %-20s Type: %s(%d)\n", + i, col->name, + mdb_get_colbacktype_string(col), + col->col_size); + if (col->props) + mdb_dump_props(col->props, stdout, 0); + } + + for (i=0;inum_idxs;i++) { + idx = g_ptr_array_index (table->indices, i); + mdb_index_dump(table, idx); + } + if (table->usage_map) { + printf("pages reserved by this object\n"); + printf("usage map pg %" G_GUINT32_FORMAT "\n", + table->map_base_pg); + printf("free map pg %" G_GUINT32_FORMAT "\n", + table->freemap_base_pg); + pgnum = mdb_get_int32(table->usage_map,1); + /* the first 5 bytes of the usage map mean something */ + coln = 0; + for (i=5;imap_sz;i++) { + for (bitn=0;bitn<8;bitn++) { + if (table->usage_map[i] & 1 << bitn) { + coln++; + printf("%6" G_GUINT32_FORMAT, pgnum); + if (coln==10) { + printf("\n"); + coln = 0; + } else { + printf(" "); + } + } + pgnum++; + } + } + printf("\n"); + } +} + +int mdb_is_user_table(MdbCatalogEntry *entry) +{ + return ((entry->object_type == MDB_TABLE) + && !(entry->flags & 0x80000002)) ? 1 : 0; +} +int mdb_is_system_table(MdbCatalogEntry *entry) +{ + return ((entry->object_type == MDB_TABLE) + && (entry->flags & 0x80000002)) ? 1 : 0; +} + +const char * +mdb_table_get_prop(const MdbTableDef *table, const gchar *key) { + if (!table->props) + return NULL; + return g_hash_table_lookup(table->props->hash, key); +} + +const char * +mdb_col_get_prop(const MdbColumn *col, const gchar *key) { + if (!col->props) + return NULL; + return g_hash_table_lookup(col->props->hash, key); +} + +int mdb_col_is_shortdate(const MdbColumn *col) { + const char *format = mdb_col_get_prop(col, "Format"); + return format && !strcmp(format, "Short Date"); +} diff --git a/wlx/dbview/src/libmdb/version.c b/wlx/dbview/src/libmdb/version.c new file mode 100644 index 0000000..25813c4 --- /dev/null +++ b/wlx/dbview/src/libmdb/version.c @@ -0,0 +1,24 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2020 Evan Miller + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mdbver.h" +#include "mdbtools.h" + +const char *mdb_get_version() { + return MDB_VERSION_NO; +} diff --git a/wlx/dbview/src/libmdb/worktable.c b/wlx/dbview/src/libmdb/worktable.c new file mode 100644 index 0000000..720ec6f --- /dev/null +++ b/wlx/dbview/src/libmdb/worktable.c @@ -0,0 +1,96 @@ +/* MDB Tools - A library for reading MS Access database files + * Copyright (C) 2004 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mdbtools.h" +#include "mdbprivate.h" + +/* + * Temp table routines. These are currently used to generate mock results for + * commands like "list tables" and "describe table" + */ + +void +mdb_fill_temp_col(MdbColumn *tcol, char *col_name, int col_size, int col_type, int is_fixed) +{ + memset(tcol,0,sizeof(MdbColumn)); + snprintf(tcol->name, sizeof(tcol->name), "%s", col_name); + tcol->col_type = col_type; + if ((col_type == MDB_TEXT) || (col_type == MDB_MEMO)) { + tcol->col_size = col_size; + } else { + tcol->col_size = mdb_col_fixed_size(tcol); + } + tcol->is_fixed = is_fixed; +} +void +mdb_fill_temp_field(MdbField *field, void *value, int siz, int is_fixed, int is_null, int start, int colnum) +{ + field->value = value; + field->siz = siz; + field->is_fixed = is_fixed; + field->is_null = is_null; + field->start = start; + field->colnum = colnum; +} +MdbTableDef * +mdb_create_temp_table(MdbHandle *mdb, char *name) +{ + MdbCatalogEntry *entry; + MdbTableDef *table; + + /* dummy up a catalog entry */ + entry = g_malloc0(sizeof(MdbCatalogEntry)); + entry->mdb = mdb; + entry->object_type = MDB_TABLE; + entry->table_pg = 0; + snprintf(entry->object_name, sizeof(entry->object_name), "%s", name); + + table = mdb_alloc_tabledef(entry); + table->columns = g_ptr_array_new(); + table->is_temp_table = 1; + table->temp_table_pages = g_ptr_array_new(); + + return table; +} +void +mdb_temp_table_add_col(MdbTableDef *table, MdbColumn *col) +{ + col->table = table, + col->col_num = table->num_cols; + if (!col->is_fixed) + col->var_col_num = table->num_var_cols++; + g_ptr_array_add(table->columns, g_memdup2(col, sizeof(MdbColumn))); + table->num_cols++; +} +/* + * Should be called after setting up all temp table columns + */ +void mdb_temp_columns_end(MdbTableDef *table) +{ + MdbColumn *col; + unsigned int i; + unsigned int start = 0; + + for (i=0; inum_cols; i++) { + col = g_ptr_array_index(table->columns, i); + if (col->is_fixed) { + col->fixed_offset = start; + start += col->col_size; + } + } +} diff --git a/wlx/dbview/src/libmdb/write.c b/wlx/dbview/src/libmdb/write.c new file mode 100644 index 0000000..44a48f2 --- /dev/null +++ b/wlx/dbview/src/libmdb/write.c @@ -0,0 +1,934 @@ +/* MDB Tools - A library for reading MS Access database file + * Copyright (C) 2000 Brian Bruns + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include "mdbprivate.h" + +//static int mdb_copy_index_pg(MdbTableDef *table, MdbIndex *idx, MdbIndexPage *ipg); +static int mdb_add_row_to_leaf_pg(MdbTableDef *table, MdbIndex *idx, MdbIndexPage *ipg, MdbField *idx_fields, guint32 pgnum, guint16 rownum); + +void +mdb_put_int16(void *buf, guint32 offset, guint32 value) +{ + unsigned char *u8_buf = (unsigned char *)buf + offset; + u8_buf[0] = (value & 0xFF); + u8_buf[1] = (value >> 8) & 0xFF; +} +void +_mdb_put_int16(void *buf, guint32 offset, guint32 value) +#ifdef HAVE_ATTRIBUTE_ALIAS +__attribute__((alias("mdb_put_int16"))); +#else +{ mdb_put_int16(buf, offset, value); } +#endif + +void +mdb_put_int32(void *buf, guint32 offset, guint32 value) +{ + unsigned char *u8_buf = (unsigned char *)buf + offset; + u8_buf[0] = (value & 0xFF); + u8_buf[1] = (value >> 8) & 0xFF; + u8_buf[2] = (value >> 16) & 0xFF; + u8_buf[3] = (value >> 24) & 0xFF; +} +void +_mdb_put_int32(void *buf, guint32 offset, guint32 value) +#ifdef HAVE_ATTRIBUTE_ALIAS +__attribute__((alias("mdb_put_int32"))); +#else +{ mdb_put_int32(buf, offset, value); } +#endif + +void +mdb_put_int32_msb(void *buf, guint32 offset, guint32 value) +{ + unsigned char *u8_buf = (unsigned char *)buf + offset; + u8_buf[3] = (value & 0xFF); + u8_buf[2] = (value >> 8) & 0xFF; + u8_buf[1] = (value >> 16) & 0xFF; + u8_buf[0] = (value >> 24) & 0xFF; +} +void +_mdb_put_int32_mdb(void *buf, guint32 offset, guint32 value) +#ifdef HAVE_ATTRIBUTE_ALIAS +__attribute__((alias("mdb_put_int32_msb"))); +#else +{ mdb_put_int32_msb(buf, offset, value); } +#endif + +ssize_t +mdb_write_pg(MdbHandle *mdb, unsigned long pg) +{ + ssize_t len; + off_t offset = pg * mdb->fmt->pg_size; + unsigned char *buf = mdb->pg_buf; + + fseeko(mdb->f->stream, 0, SEEK_END); + /* is page beyond current size + 1 ? */ + if (ftello(mdb->f->stream) < offset + mdb->fmt->pg_size) { + fprintf(stderr,"offset %" PRIu64 " is beyond EOF\n",(uint64_t)offset); + return 0; + } + fseeko(mdb->f->stream, offset, SEEK_SET); + + if (pg != 0 && mdb->f->db_key != 0) + { + buf = g_memdup2(mdb->pg_buf, mdb->fmt->pg_size); + unsigned int tmp_key = mdb->f->db_key ^ pg; + mdbi_rc4((unsigned char*)&tmp_key, 4, buf, mdb->fmt->pg_size); + } + + len = fwrite(buf, 1, mdb->fmt->pg_size, mdb->f->stream); + + if (buf != mdb->pg_buf) { + g_free(buf); + } + + if (ferror(mdb->f->stream)) { + perror("write"); + return 0; + } else if (lenfmt->pg_size) { + /* fprintf(stderr,"EOF reached %d bytes returned.\n",len, mdb->pg_size); */ + return 0; + } + mdb->cur_pos = 0; + return len; +} + +static int +mdb_is_col_indexed(MdbTableDef *table, int colnum) +{ + unsigned int i, j; + MdbIndex *idx; + + for (i=0;inum_idxs;i++) { + idx = g_ptr_array_index (table->indices, i); + for (j=0;jnum_keys;j++) { + if (idx->key_col_num[j]==colnum) return 1; + } + } + return 0; +} + +static int +mdb_crack_row4(MdbHandle *mdb, unsigned int row_start, unsigned int row_end, + unsigned int bitmask_sz, unsigned int row_var_cols, unsigned int *var_col_offsets) +{ + unsigned int i; + + if (bitmask_sz + 3 + row_var_cols*2 + 2 > row_end) + return 0; + + for (i=0; ipg_buf, + row_end - bitmask_sz - 3 - (i*2)); + } + + return 1; +} +static int +mdb_crack_row3(MdbHandle *mdb, unsigned int row_start, unsigned int row_end, + unsigned int bitmask_sz, unsigned int row_var_cols, unsigned int *var_col_offsets) +{ + unsigned int i; + unsigned int num_jumps = 0, jumps_used = 0; + unsigned int col_ptr, row_len; + + row_len = row_end - row_start + 1; + num_jumps = (row_len - 1) / 256; + col_ptr = row_end - bitmask_sz - num_jumps - 1; + /* If last jump is a dummy value, ignore it */ + if ((col_ptr-row_start-row_var_cols)/256 < num_jumps) + num_jumps--; + + if (bitmask_sz + num_jumps + 1 > row_end) + return 0; + + if (col_ptr >= (size_t)mdb->fmt->pg_size || col_ptr < row_var_cols) + return 0; + + jumps_used = 0; + for (i=0; ipg_buf[row_end-bitmask_sz-jumps_used-1])) { + jumps_used++; + } + var_col_offsets[i] = mdb->pg_buf[col_ptr-i]+(jumps_used*256); + } + + return 1; +} +/** + * @brief Cracks a row buffer apart into its component fields. + * @param table: Table that the row belongs to + * @param row_start: offset to start of row on current page + * @param row_size: offset to end of row on current page + * @param fields: pointer to MdbField array to be populated by mdb_crack_row + * + * + * A row buffer is that portion of a data page which contains the values for + * that row. Its beginning and end can be found in the row offset table. + * + * The resulting MdbField array contains pointers into the row for each field + * present. Be aware that by modifying field[]->value, you would be modifying + * the row buffer itself, not a copy. + * + * This routine is mostly used internally by mdb_fetch_row() but may have some + * applicability for advanced application programs. + * + * @return: number of fields present, or -1 if the buffer is invalid. + */ +int +mdb_crack_row(MdbTableDef *table, int row_start, size_t row_size, MdbField *fields) +{ + MdbColumn *col; + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + void *pg_buf = mdb->pg_buf; + unsigned int row_var_cols=0, row_cols; + unsigned char *nullmask; + unsigned int bitmask_sz; + unsigned int *var_col_offsets = NULL; + unsigned int fixed_cols_found, row_fixed_cols; + unsigned int col_count_size; + unsigned int i; + unsigned int row_end = row_start + row_size - 1; + + if (mdb_get_option(MDB_DEBUG_ROW)) { + mdb_buffer_dump(pg_buf, row_start, row_size); + } + + if (IS_JET3(mdb)) { + row_cols = mdb_get_byte(pg_buf, row_start); + col_count_size = 1; + } else { + row_cols = mdb_get_int16(pg_buf, row_start); + col_count_size = 2; + } + + bitmask_sz = (row_cols + 7) / 8; + if (bitmask_sz + !IS_JET3(mdb) >= row_end) { + fprintf(stderr, "warning: Invalid page buffer detected in mdb_crack_row.\n"); + return -1; + } + + nullmask = (unsigned char*)pg_buf + row_end - bitmask_sz + 1; + + /* read table of variable column locations */ + if (table->num_var_cols > 0) { + row_var_cols = IS_JET3(mdb) ? + mdb_get_byte(pg_buf, row_end - bitmask_sz) : + mdb_get_int16(pg_buf, row_end - bitmask_sz - 1); + var_col_offsets = g_malloc((row_var_cols+1)*sizeof(int)); + int success = 0; + if (IS_JET3(mdb)) { + success = mdb_crack_row3(mdb, row_start, row_end, bitmask_sz, + row_var_cols, var_col_offsets); + } else { + success = mdb_crack_row4(mdb, row_start, row_end, bitmask_sz, + row_var_cols, var_col_offsets); + } + if (!success) { + fprintf(stderr, "warning: Invalid page buffer detected in mdb_crack_row.\n"); + g_free(var_col_offsets); + return -1; + } + } + + fixed_cols_found = 0; + row_fixed_cols = row_cols - row_var_cols; + + if (mdb_get_option(MDB_DEBUG_ROW)) { + fprintf(stdout,"bitmask_sz %d\n", bitmask_sz); + fprintf(stdout,"row_var_cols %d\n", row_var_cols); + fprintf(stdout,"row_fixed_cols %d\n", row_fixed_cols); + } + + for (i=0;inum_cols;i++) { + unsigned int byte_num, bit_num; + unsigned int col_start; + col = g_ptr_array_index(table->columns,i); + fields[i].colnum = i; + fields[i].is_fixed = col->is_fixed; + byte_num = col->col_num / 8; + bit_num = col->col_num % 8; + /* logic on nulls is reverse, 1 is not null, 0 is null */ + fields[i].is_null = nullmask[byte_num] & (1 << bit_num) ? 0 : 1; + + if ((fields[i].is_fixed) + && (fixed_cols_found < row_fixed_cols)) { + col_start = col->fixed_offset + col_count_size; + fields[i].start = row_start + col_start; + fields[i].value = (char*)pg_buf + row_start + col_start; + fields[i].siz = col->col_size; + fixed_cols_found++; + /* Use col->var_col_num because a deleted column is still + * present in the variable column offsets table for the row */ + } else if ((!fields[i].is_fixed) + && (col->var_col_num < row_var_cols)) { + col_start = var_col_offsets[col->var_col_num]; + fields[i].start = row_start + col_start; + fields[i].value = (char*)pg_buf + row_start + col_start; + fields[i].siz = var_col_offsets[(col->var_col_num)+1] - + col_start; + } else { + fields[i].start = 0; + fields[i].value = NULL; + fields[i].siz = 0; + fields[i].is_null = 1; + } + if ((size_t)(fields[i].start + fields[i].siz) > row_start + row_size) { + fprintf(stderr, "warning: Invalid data location detected in mdb_crack_row. Table:%s Column:%i\n",table->name, i); + g_free(var_col_offsets); + return -1; + } + } + + g_free(var_col_offsets); + return row_cols; +} + +static int +mdb_pack_null_mask(unsigned char *buffer, int num_fields, MdbField *fields) +{ + int pos = 0, bit = 0, byte = 0; + int i; + + /* 'Not null' bitmap */ + for (i=0; i> 8) & 0xff; + + /* Fixed length columns */ + for (i=0;inum_var_cols == 0) { + pos += mdb_pack_null_mask(&row_buffer[pos], num_fields, fields); + return pos; + } + /* Variable length columns */ + for (i=0;i> 8) & 0xff; + pos += 2; + + /* Offsets of the variable-length columns */ + for (i=num_fields; i>0; i--) { + if (!fields[i-1].is_fixed) { + row_buffer[pos++] = fields[i-1].offset & 0xff; + row_buffer[pos++] = (fields[i-1].offset >> 8) & 0xff; + } + } + /* Number of variable-length columns */ + row_buffer[pos++] = var_cols & 0xff; + row_buffer[pos++] = (var_cols >> 8) & 0xff; + + pos += mdb_pack_null_mask(&row_buffer[pos], num_fields, fields); + return pos; +} + +static int +mdb_pack_row3(MdbTableDef *table, unsigned char *row_buffer, unsigned int num_fields, MdbField *fields) +{ + unsigned int pos = 0; + unsigned int var_cols = 0; + unsigned int i, j; + unsigned char *offset_high; + + row_buffer[pos++] = num_fields; + + /* Fixed length columns */ + for (i=0;inum_var_cols == 0) { + pos += mdb_pack_null_mask(&row_buffer[pos], num_fields, fields); + return pos; + } + /* Variable length columns */ + for (i=0;i> 8) & 0xff; + j = 1; + + /* EOD */ + row_buffer[pos] = pos & 0xff; + pos++; + + /* Variable length column offsets */ + for (i=num_fields; i>0; i--) { + if (!fields[i-1].is_fixed) { + row_buffer[pos++] = fields[i-1].offset & 0xff; + offset_high[j++] = (fields[i-1].offset >> 8) & 0xff; + } + } + + /* Dummy jump table entry */ + if (offset_high[0] < (pos+(num_fields+7)/8-1)/255) { + row_buffer[pos++] = 0xff; + } + /* Jump table */ + for (i=0; i offset_high[i+1]) { + row_buffer[pos++] = var_cols-i; + } + } + g_free(offset_high); + + row_buffer[pos++] = var_cols; + + pos += mdb_pack_null_mask(&row_buffer[pos], num_fields, fields); + return pos; +} +int +mdb_pack_row(MdbTableDef *table, unsigned char *row_buffer, int unsigned num_fields, MdbField *fields) +{ + if (table->is_temp_table) { + unsigned int i; + for (i=0; icolumns, i); + fields[i].is_null = (fields[i].value) ? 0 : 1; + fields[i].colnum = i; + fields[i].is_fixed = c->is_fixed; + if ((c->col_type != MDB_TEXT) + && (c->col_type != MDB_MEMO)) { + fields[i].siz = c->col_size; + } + } + } + if (IS_JET3(table->entry->mdb)) { + return mdb_pack_row3(table, row_buffer, num_fields, fields); + } else { + return mdb_pack_row4(table, row_buffer, num_fields, fields); + } +} +int +mdb_pg_get_freespace(MdbHandle *mdb) +{ + int rows, free_start, free_end; + int row_count_offset = mdb->fmt->row_count_offset; + + rows = mdb_get_int16(mdb->pg_buf, row_count_offset); + free_start = row_count_offset + 2 + (rows * 2); + free_end = mdb_get_int16(mdb->pg_buf, row_count_offset + (rows * 2)); + mdb_debug(MDB_DEBUG_WRITE,"free space left on page = %d", free_end - free_start); + return (free_end - free_start); +} +void * +mdb_new_leaf_pg(MdbCatalogEntry *entry) +{ + MdbHandle *mdb = entry->mdb; + void *new_pg = g_malloc0(mdb->fmt->pg_size); + + mdb_put_int16(new_pg, 0, 0x0104); + mdb_put_int32(new_pg, 4, entry->table_pg); + + return new_pg; +} +void * +mdb_new_data_pg(MdbCatalogEntry *entry) +{ + MdbFormatConstants *fmt = entry->mdb->fmt; + void *new_pg = g_malloc0(fmt->pg_size); + + mdb_put_int16(new_pg, 0, 0x0101); + mdb_put_int16(new_pg, 2, fmt->pg_size - fmt->row_count_offset - 2); + mdb_put_int32(new_pg, 4, entry->table_pg); + + return new_pg; +} + +/* could be static */ +int +mdb_update_indexes(MdbTableDef *table, int num_fields, MdbField *fields, guint32 pgnum, guint16 rownum) +{ + unsigned int i; + MdbIndex *idx; + + for (i=0;inum_idxs;i++) { + idx = g_ptr_array_index (table->indices, i); + mdb_debug(MDB_DEBUG_WRITE,"Updating %s (%d).", idx->name, idx->index_type); + if (idx->index_type==1) { + mdb_update_index(table, idx, num_fields, fields, pgnum, rownum); + } + } + return 1; +} + +int +mdb_init_index_chain(MdbTableDef *table, MdbIndex *idx) +{ + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + + table->scan_idx = idx; + table->chain = g_malloc0(sizeof(MdbIndexChain)); + table->mdbidx = mdb_clone_handle(mdb); + mdb_read_pg(table->mdbidx, table->scan_idx->first_pg); + + return 1; +} + +/* could be static */ +int +mdb_update_index(MdbTableDef *table, MdbIndex *idx, unsigned int num_fields, MdbField *fields, guint32 pgnum, guint16 rownum) +{ + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + /*int idx_xref[16];*/ + unsigned int i, j; + MdbIndexChain *chain; + MdbField idx_fields[10]; + + for (i = 0; i < idx->num_keys; i++) { + for (j = 0; j < num_fields; j++) { + // key_col_num is 1 based, can't remember why though + if (fields[j].colnum == idx->key_col_num[i]-1) { + /* idx_xref[i] = j; */ + idx_fields[i] = fields[j]; + } + } + } +/* + for (i = 0; i < idx->num_keys; i++) { + fprintf(stdout, "key col %d (%d) is mapped to field %d (%d %d)\n", + i, idx->key_col_num[i], idx_xref[i], fields[idx_xref[i]].colnum, + fields[idx_xref[i]].siz); + } + for (i = 0; i < num_fields; i++) { + fprintf(stdout, "%d (%d %d)\n", + i, fields[i].colnum, + fields[i].siz); + } +*/ + + chain = g_malloc0(sizeof(MdbIndexChain)); + + mdb_index_find_row(mdb, idx, chain, pgnum, rownum); + //printf("chain depth = %d\n", chain->cur_depth); + //printf("pg = %" G_GUINT32_FORMAT "\n", + //chain->pages[chain->cur_depth-1].pg); + //mdb_copy_index_pg(table, idx, &chain->pages[chain->cur_depth-1]); + mdb_add_row_to_leaf_pg(table, idx, &chain->pages[chain->cur_depth-1], idx_fields, pgnum, rownum); + + return 1; +} + +int +mdb_insert_row(MdbTableDef *table, int num_fields, MdbField *fields) +{ + int new_row_size; + unsigned char row_buffer[4096]; + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + MdbFormatConstants *fmt = mdb->fmt; + gint32 pgnum; + guint16 rownum; + + if (!mdb->f->writable) { + fprintf(stderr, "File is not open for writing\n"); + return 0; + } + new_row_size = mdb_pack_row(table, row_buffer, num_fields, fields); + if (mdb_get_option(MDB_DEBUG_WRITE)) { + mdb_buffer_dump(row_buffer, 0, new_row_size); + } + pgnum = mdb_map_find_next_freepage(table, new_row_size); + if (!pgnum || pgnum == -1) { + fprintf(stderr, "Unable to allocate new page.\n"); + return 0; + } + + rownum = mdb_add_row_to_pg(table, row_buffer, new_row_size); + + if (mdb_get_option(MDB_DEBUG_WRITE)) { + mdb_buffer_dump(mdb->pg_buf, 0, 40); + mdb_buffer_dump(mdb->pg_buf, fmt->pg_size - 160, 160); + } + mdb_debug(MDB_DEBUG_WRITE, "writing page %d", pgnum); + if (!mdb_write_pg(mdb, pgnum)) { + fprintf(stderr, "write failed!\n"); + return 0; + } + + mdb_update_indexes(table, num_fields, fields, pgnum, rownum); + + return 1; +} +/* + * Assumes caller has verfied space is available on page and adds the new + * row to the current pg_buf. + */ +guint16 +mdb_add_row_to_pg(MdbTableDef *table, unsigned char *row_buffer, int new_row_size) +{ + void *new_pg; + int num_rows, i, pos, row_start; + size_t row_size; + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + MdbFormatConstants *fmt = mdb->fmt; + + if (table->is_temp_table) { + GPtrArray *pages = table->temp_table_pages; + if (pages->len == 0) { + new_pg = mdb_new_data_pg(entry); + g_ptr_array_add(pages, new_pg); + } else { + new_pg = g_ptr_array_index(pages, pages->len - 1); + if (mdb_get_int16(new_pg, 2) < new_row_size + 2) { + new_pg = mdb_new_data_pg(entry); + g_ptr_array_add(pages, new_pg); + } + } + + num_rows = mdb_get_int16(new_pg, fmt->row_count_offset); + pos = (num_rows == 0) ? fmt->pg_size : + mdb_get_int16(new_pg, fmt->row_count_offset + (num_rows*2)); + } else { /* is not a temp table */ + new_pg = mdb_new_data_pg(entry); + + num_rows = mdb_get_int16(mdb->pg_buf, fmt->row_count_offset); + pos = fmt->pg_size; + + /* copy existing rows */ + for (i=0;ipg_buf + row_start, row_size); + mdb_put_int16(new_pg, (fmt->row_count_offset + 2) + (i*2), pos); + } + } + + /* add our new row */ + pos -= new_row_size; + memcpy((char*)new_pg + pos, row_buffer, new_row_size); + /* add row to the row offset table */ + mdb_put_int16(new_pg, (fmt->row_count_offset + 2) + (num_rows*2), pos); + + /* update number rows on this page */ + num_rows++; + mdb_put_int16(new_pg, fmt->row_count_offset, num_rows); + + /* update the freespace */ + mdb_put_int16(new_pg,2,pos - fmt->row_count_offset - 2 - (num_rows*2)); + + /* copy new page over old */ + if (!table->is_temp_table) { + memcpy(mdb->pg_buf, new_pg, fmt->pg_size); + g_free(new_pg); + } + + return num_rows; +} +int +mdb_update_row(MdbTableDef *table) +{ + int row_start, row_end; + unsigned int i; + MdbColumn *col; + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + MdbField fields[256]; + unsigned char row_buffer[4096]; + size_t old_row_size, new_row_size; + int num_fields; + + if (!mdb->f->writable) { + fprintf(stderr, "File is not open for writing\n"); + return 0; + } + mdb_find_row(mdb, table->cur_row-1, &row_start, &old_row_size); + row_end = row_start + old_row_size - 1; + + row_start &= 0x0FFF; /* remove flags */ + + mdb_debug(MDB_DEBUG_WRITE,"page %lu row %d start %d end %d", (unsigned long) table->cur_phys_pg, table->cur_row-1, row_start, row_end); + if (mdb_get_option(MDB_DEBUG_LIKE)) + mdb_buffer_dump(mdb->pg_buf, row_start, old_row_size); + + for (i=0;inum_cols;i++) { + col = g_ptr_array_index(table->columns,i); + if (col->bind_ptr && mdb_is_col_indexed(table,i)) { + fprintf(stderr, "Attempting to update column that is part of an index\n"); + return 0; + } + } + num_fields = mdb_crack_row(table, row_start, old_row_size, fields); + if (num_fields == -1) { + fprintf(stderr, "Invalid row buffer, update will not occur\n"); + return 0; + } + + if (mdb_get_option(MDB_DEBUG_WRITE)) { + /* + for (i=0;inum_cols;i++) { + col = g_ptr_array_index(table->columns,i); + if (col->bind_ptr) { + fields[i].value = col->bind_ptr; + fields[i].siz = *(col->len_ptr); + } + } + + new_row_size = mdb_pack_row(table, row_buffer, num_fields, fields); + if (mdb_get_option(MDB_DEBUG_WRITE)) + mdb_buffer_dump(row_buffer, 0, new_row_size); + if (new_row_size > (old_row_size + mdb_pg_get_freespace(mdb))) { + fprintf(stderr, "No space left on this page, update will not occur\n"); + return 0; + } + /* do it! */ + mdb_replace_row(table, table->cur_row-1, row_buffer, new_row_size); + return 0; /* FIXME */ +} + +/** + * \warning the return code is opposite to convention used elsewhere: + * @return: + * - 0 on success + * - 1 on failure + * \note This might change on next ABI break. + */ +int +mdb_replace_row(MdbTableDef *table, int row, void *new_row, int new_row_size) +{ +MdbCatalogEntry *entry = table->entry; +MdbHandle *mdb = entry->mdb; +int pg_size = mdb->fmt->pg_size; +int rco = mdb->fmt->row_count_offset; + void *new_pg; +guint16 num_rows; + int row_start; + size_t row_size; +int i, pos; + + if (mdb_get_option(MDB_DEBUG_WRITE)) { + mdb_buffer_dump(mdb->pg_buf, 0, 40); + mdb_buffer_dump(mdb->pg_buf, pg_size - 160, 160); + } + mdb_debug(MDB_DEBUG_WRITE,"updating row %d on page %lu", row, (unsigned long) table->cur_phys_pg); + new_pg = mdb_new_data_pg(entry); + + num_rows = mdb_get_int16(mdb->pg_buf, rco); + mdb_put_int16(new_pg, rco, num_rows); + + pos = pg_size; + + /* rows before */ + for (i=0;ipg_buf + row_start, row_size); + mdb_put_int16(new_pg, rco + 2 + i*2, pos); + } + + /* our row */ + pos -= new_row_size; + memcpy((char*)new_pg + pos, new_row, new_row_size); + mdb_put_int16(new_pg, rco + 2 + row*2, pos); + + /* rows after */ + for (i=row+1;ipg_buf + row_start, row_size); + mdb_put_int16(new_pg, rco + 2 + i*2, pos); + } + + /* almost done, copy page over current */ + memcpy(mdb->pg_buf, new_pg, pg_size); + + g_free(new_pg); + + mdb_put_int16(mdb->pg_buf, 2, mdb_pg_get_freespace(mdb)); + if (mdb_get_option(MDB_DEBUG_WRITE)) { + mdb_buffer_dump(mdb->pg_buf, 0, 40); + mdb_buffer_dump(mdb->pg_buf, pg_size - 160, 160); + } + /* drum roll, please */ + if (!mdb_write_pg(mdb, table->cur_phys_pg)) { + fprintf(stderr, "write failed!\n"); + return 1; + } + return 0; +} +static int +mdb_add_row_to_leaf_pg(MdbTableDef *table, MdbIndex *idx, MdbIndexPage *ipg, MdbField *idx_fields, guint32 pgnum, guint16 rownum) +/*, guint32 pgnum, guint16 rownum) +static int +mdb_copy_index_pg(MdbTableDef *table, MdbIndex *idx, MdbIndexPage *ipg) +*/ +{ + MdbCatalogEntry *entry = table->entry; + MdbHandle *mdb = entry->mdb; + MdbColumn *col; + guint32 pg_row; + guint16 row = 0; + void *new_pg; + unsigned char key_hash[256]; + int keycol; + + new_pg = mdb_new_leaf_pg(entry); + + /* reinitial ipg pointers to start of page */ + mdb_index_page_reset(mdb, ipg); + mdb_read_pg(mdb, ipg->pg); + + /* do we support this index type yet? */ + if (idx->num_keys > 1) { + fprintf(stderr,"multikey indexes not yet supported, aborting\n"); + return 0; + } + keycol = idx->key_col_num[0]; + col = g_ptr_array_index (table->columns, keycol - 1); + if (!col->is_fixed) { + fprintf(stderr,"variable length key columns not yet supported, aborting\n"); + return 0; + } + + while (mdb_index_find_next_on_page(mdb, ipg)) { + + /* check for compressed indexes. */ + if (ipg->len < col->col_size + 1) { + fprintf(stderr,"compressed indexes not yet supported, aborting\n"); + return 0; + } + + pg_row = mdb_get_int32_msb(mdb->pg_buf, ipg->offset + ipg->len - 4); + /* guint32 pg = pg_row >> 8; */ + row = pg_row & 0xff; + /* unsigned char iflag = mdb->pg_buf[ipg->offset]; */ + + /* turn the key hash back into a value */ + mdb_index_swap_n(&mdb->pg_buf[ipg->offset + 1], col->col_size, key_hash); + key_hash[col->col_size - 1] &= 0x7f; + + if (mdb_get_option(MDB_DEBUG_WRITE)) { + mdb_buffer_dump(mdb->pg_buf, ipg->offset, ipg->len); + mdb_buffer_dump(mdb->pg_buf, ipg->offset + 1, col->col_size); + mdb_buffer_dump(key_hash, 0, col->col_size); + } + + memcpy((char*)new_pg + ipg->offset, mdb->pg_buf + ipg->offset, ipg->len); + ipg->offset += ipg->len; + ipg->len = 0; + + row++; + } + + if (!row) { + fprintf(stderr,"missing indexes not yet supported, aborting\n"); + return 0; + } + //mdb_put_int16(new_pg, mdb->fmt->row_count_offset, row); + /* free space left */ + mdb_put_int16(new_pg, 2, mdb->fmt->pg_size - ipg->offset); + //printf("offset = %d\n", ipg->offset); + + mdb_index_swap_n(idx_fields[0].value, col->col_size, key_hash); + key_hash[0] |= 0x080; + if (mdb_get_option(MDB_DEBUG_WRITE)) { + printf("key_hash\n"); + mdb_buffer_dump(idx_fields[0].value, 0, col->col_size); + mdb_buffer_dump(key_hash, 0, col->col_size); + printf("--------\n"); + } + ((char *)new_pg)[ipg->offset] = 0x7f; + memcpy((char*)new_pg + ipg->offset + 1, key_hash, col->col_size); + pg_row = (pgnum << 8) | ((rownum-1) & 0xff); + mdb_put_int32_msb(new_pg, ipg->offset + 5, pg_row); + ipg->idx_starts[row++] = ipg->offset + ipg->len; + //ipg->idx_starts[row] = ipg->offset + ipg->len; + if (mdb_get_option(MDB_DEBUG_WRITE)) { + mdb_buffer_dump(mdb->pg_buf, 0, mdb->fmt->pg_size); + } + memcpy(mdb->pg_buf, new_pg, mdb->fmt->pg_size); + mdb_index_pack_bitmap(mdb, ipg); + if (mdb_get_option(MDB_DEBUG_WRITE)) { + mdb_buffer_dump(mdb->pg_buf, 0, mdb->fmt->pg_size); + } + g_free(new_pg); + + return ipg->len; +} diff --git a/wlx/dbview/src/wlx_entry.cpp b/wlx/dbview/src/wlx_entry.cpp new file mode 100644 index 0000000..b47c9ac --- /dev/null +++ b/wlx/dbview/src/wlx_entry.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "wlxplugin.h" +#include "DbViewWidget.h" + +// --------------------------------------------------------------------------- +// WLX Plugin Entry Points — all wrapped with WLX_TRY/WLX_CATCH +// --------------------------------------------------------------------------- + +HWND DCPCALL ListLoad(HWND ParentWin, char* FileToLoad, int ShowFlags) +{ + WLX_TRY { + Q_UNUSED(ShowFlags); + if (!QApplication::instance()) + return nullptr; + + auto *widget = new DbViewWidget(reinterpret_cast(ParentWin)); + if (!widget->loadFile(QString::fromUtf8(FileToLoad))) { + delete widget; + return nullptr; + } + + widget->show(); + return reinterpret_cast(widget); + } WLX_CATCH("ListLoad"); + return nullptr; +} + +void DCPCALL ListCloseWindow(HWND ListWin) +{ + WLX_TRY { + auto *widget = reinterpret_cast(ListWin); + delete widget; + } WLX_CATCH("ListCloseWindow"); +} + +int DCPCALL ListSendCommand(HWND ListWin, int Command, int Parameter) +{ + WLX_TRY { + auto *widget = reinterpret_cast(ListWin); + + switch (Command) { + case lc_copy: { + QString text = widget->getSelectionAsText('\t'); + if (text.isEmpty()) return LISTPLUGIN_ERROR; + QApplication::clipboard()->setText(text); + break; + } + case lc_selectall: + if (widget->grid() && widget->grid()->view()) + widget->grid()->view()->selectAll(); + break; + case lc_focus: + if (Parameter) { + widget->focusManager()->setActive(true); + if (widget->grid() && widget->grid()->view()) + widget->grid()->view()->setFocus(Qt::OtherFocusReason); + } else { + widget->focusManager()->setActive(false); + if (QWidget *fw = QApplication::focusWidget()) { + if (fw == widget || widget->isAncestorOf(fw)) + fw->clearFocus(); + } + } + break; + default: + return LISTPLUGIN_ERROR; + } + return LISTPLUGIN_OK; + } WLX_CATCH("ListSendCommand"); + return LISTPLUGIN_ERROR; +} + +int DCPCALL ListSearchText(HWND ListWin, char* SearchString, int SearchParameter) +{ + WLX_TRY { + auto *widget = reinterpret_cast(ListWin); + if (!widget->grid() || !widget->grid()->view() || !widget->grid()->view()->model()) + return LISTPLUGIN_ERROR; + + QAbstractItemModel *model = widget->grid()->view()->model(); + QString needle = QString::fromUtf8(SearchString); + QTableView *view = widget->grid()->view(); + + // Track search state + QString prev = view->property("needle").value(); + view->setProperty("needle", needle); + + QModelIndex current = view->currentIndex(); + int startRow = 0, startCol = 0; + if (current.isValid()) { + startRow = current.row(); + startCol = current.column(); + } + + bool first = (needle != prev) || (SearchParameter & lcs_findfirst); + bool backward = (SearchParameter & lcs_backwards); + + if (!first) { + if (backward) { + startCol--; + if (startCol < 0) { startCol = model->columnCount() - 1; startRow--; } + } else { + startCol++; + if (startCol >= model->columnCount()) { startCol = 0; startRow++; } + } + } + + int rows = model->rowCount(); + int cols = model->columnCount(); + int total = rows * cols; + + for (int i = 0; i < total; ++i) { + int offset = backward ? -i : i; + int linearStart = startRow * cols + startCol; + int linearIdx = (linearStart + offset + total) % total; + int r = linearIdx / cols; + int c = linearIdx % cols; + + QModelIndex idx = model->index(r, c); + QString cellText = model->data(idx, Qt::DisplayRole).toString(); + + bool match = (SearchParameter & lcs_matchcase) + ? cellText.contains(needle, Qt::CaseSensitive) + : cellText.contains(needle, Qt::CaseInsensitive); + + if (match) { + view->setCurrentIndex(idx); + view->scrollTo(idx); + return LISTPLUGIN_OK; + } + } + + return LISTPLUGIN_ERROR; + } WLX_CATCH("ListSearchText"); + return LISTPLUGIN_ERROR; +} + +void DCPCALL ListGetDetectString(char* DetectString, int maxlen) +{ +#ifdef ENABLE_ROCKSDB_LEVELDB + snprintf(DetectString, maxlen - 1, + "EXT=\"DB\" | EXT=\"SQLITE\" | EXT=\"SQLITE3\" | EXT=\"DB3\" | " + "EXT=\"DUCKDB\" | EXT=\"LDB\" | EXT=\"SST\" | EXT=\"LMDB\" | " + "EXT=\"BDB\" | EXT=\"FDB\" | EXT=\"MDB\" | EXT=\"ACCDB\" | " + "EXT=\"PARQUET\" | EXT=\"PQ\""); +#else + snprintf(DetectString, maxlen - 1, + "EXT=\"DB\" | EXT=\"SQLITE\" | EXT=\"SQLITE3\" | EXT=\"DB3\" | " + "EXT=\"DUCKDB\" | EXT=\"LMDB\" | " + "EXT=\"BDB\" | EXT=\"FDB\" | EXT=\"MDB\" | EXT=\"ACCDB\" | " + "EXT=\"PARQUET\" | EXT=\"PQ\""); +#endif +} diff --git a/wlx/wlxbase_wlqt/CMakeLists.txt b/wlx/wlxbase_wlqt/CMakeLists.txt new file mode 100644 index 0000000..3b1255f --- /dev/null +++ b/wlx/wlxbase_wlqt/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.16) +project(wlxbase_wlqt LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB2 REQUIRED glib-2.0) + +# --------------------------------------------------------------------------- +# enca — downloaded and built as a static library +# --------------------------------------------------------------------------- +set(ENCA_VERSION "1.19") +set(ENCA_DIR "${CMAKE_CURRENT_BINARY_DIR}/enca-${ENCA_VERSION}") +set(ENCA_TAR "${CMAKE_CURRENT_BINARY_DIR}/enca-${ENCA_VERSION}.tar.gz") +set(ENCA_LIB "${ENCA_DIR}/lib/.libs/libenca.a") + +if(NOT EXISTS "${ENCA_LIB}") + if(NOT EXISTS "${ENCA_TAR}") + message(STATUS "Downloading enca ${ENCA_VERSION}...") + file(DOWNLOAD + "https://dl.cihar.com/enca/enca-${ENCA_VERSION}.tar.gz" + "${ENCA_TAR}" + SHOW_PROGRESS + ) + endif() + message(STATUS "Extracting and building enca...") + execute_process(COMMAND ${CMAKE_COMMAND} -E tar xzf "${ENCA_TAR}" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + execute_process(COMMAND ./configure --disable-shared --enable-static --with-pic + WORKING_DIRECTORY "${ENCA_DIR}") + execute_process(COMMAND make -j4 + WORKING_DIRECTORY "${ENCA_DIR}") +endif() + +add_library(enca_static STATIC IMPORTED) +set_target_properties(enca_static PROPERTIES + IMPORTED_LOCATION "${ENCA_LIB}" +) + +# --------------------------------------------------------------------------- +# wlxbase_wlqt static library +# --------------------------------------------------------------------------- +file(GLOB BASE_HEADERS "include/wlxbase_wlqt/*.h") + +add_library(wlxbase_wlqt STATIC + ${BASE_HEADERS} + src/FocusManager.cpp + src/PluginToolBar.cpp + src/EditableGridWidget.cpp + src/FindReplacePanel.cpp + src/ScopedFindReplacePanel.cpp + src/EncodingUtils.cpp + src/FilterRowWidget.cpp + src/FilterableHeaderView.cpp + src/PluginStatusBar.cpp + src/PluginSplitView.cpp + src/ThemeManager.cpp + src/CrashLogger.cpp + src/SequentialRowProxyModel.cpp +) + +target_include_directories(wlxbase_wlqt + PUBLIC + include + PRIVATE + "${ENCA_DIR}/lib" + ${GLIB2_INCLUDE_DIRS} +) + +target_link_libraries(wlxbase_wlqt + PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets + PRIVATE + enca_static + ${GLIB2_LIBRARIES} + ${CMAKE_DL_LIBS} +) diff --git a/wlx/wlxbase_wlqt/README.md b/wlx/wlxbase_wlqt/README.md new file mode 100644 index 0000000..c7d87f7 --- /dev/null +++ b/wlx/wlxbase_wlqt/README.md @@ -0,0 +1,810 @@ +# wayland_qt_base + +A reusable component library for building Qt6 Wayland WLX plugins for [Double Commander](https://doublecmd.sourceforge.io/). + +The library codifies battle-tested patterns that evolved across `csvview`, `logview`, and `kate` — focus management, keyboard shortcut handling, toolbar integration, grid editing, find/replace, and encoding detection — into a clean, decoupled foundation for new plugins. + +> **Existing plugins are not modified by this library.** This is a new foundation for future plugin development. + +--- + +## Table of Contents + +- [Architecture](#architecture) +- [Requirements](#requirements) +- [Building](#building) +- [Integration](#integration) +- [Components](#components) + - [FocusManager](#focusmanager) + - [PluginToolBar](#plugintoolbar) + - [EditableGridWidget](#editablegridwidget) + - [FindReplacePanel](#findreplacepanel) + - [ScopedFindReplacePanel](#scopedfindreplacepanel) +- [Utilities](#utilities) + - [EncodingUtils](#encodingutils) +- [Examples](#examples) +- [Design Decisions](#design-decisions) +- [Directory Layout](#directory-layout) + +--- + +## Architecture + +Each component depends only on `FocusManager`. A plugin that needs just a toolbar doesn't pull in grid or find/replace. `EncodingUtils` is a standalone utility with no component dependencies. + +``` + ┌──────────────────────┐ + │ FocusManager │ + │ (core framework) │ + │ shortcut registry │ + │ optional undo/redo │ + └──────┬───┬───┬───────┘ + │ │ │ + ┌───────────┘ │ └───────────┐ + │ │ │ + ┌─────────▼──┐ ┌────────▼────────┐ ┌───▼──────────────┐ + │PluginToolBar│ │EditableGridWidget│ │ FindReplacePanel │ + │(focus-safe) │ │ (grid + undo) │ │ (base, no scope)│ + └────────────┘ └─────────────────┘ └───────┬──────────┘ + │ extends + ┌───────▼───────────────┐ + │ScopedFindReplacePanel │ + │(adds scope combo box) │ + └───────────────────────┘ + + ┌─────────────┐ + │EncodingUtils │ (standalone utility — no component deps) + └─────────────┘ +``` + +**Namespace:** `QtWlPlugin` + +--- + +## Requirements + +- **CMake** ≥ 3.16 +- **Qt 6** (Core, Gui, Widgets) +- **GLib 2.0** (for encoding conversion via `g_convert_with_fallback`) +- **C++20** compiler +- **Internet connection** on first build (to download enca 1.19) + +The [enca](https://cihar.com/software/enca/) library is downloaded and statically built automatically during the CMake configure step. + +--- + +## Building + +### Standalone + +```bash +cd wlx/wayland_qt_base +mkdir build && cd build +cmake .. +make -j$(nproc) +``` + +This produces `libwayland_qt_base.a` — a static library. Each consumer plugin links it in and produces a self-contained `.wlx`. + +### As part of a plugin build + +In your plugin's `CMakeLists.txt`: + +```cmake +add_subdirectory(../../wayland_qt_base wayland_qt_base) +target_link_libraries(my_plugin PRIVATE wayland_qt_base) +``` + +The public include directory (`include/`) is automatically added, so you can include headers as: + +```cpp +#include +#include +// etc. +``` + +--- + +## Integration + +A minimal plugin using this library: + +```cpp +#include +#include + +class MyPluginWidget : public QWidget { + Q_OBJECT +public: + MyPluginWidget(QWidget *parent = nullptr) : QWidget(parent) { + auto *view = new QTextEdit(this); + auto *layout = new QVBoxLayout(this); + + // 1. Create FocusManager (required for all components) + m_fm = new QtWlPlugin::FocusManager(this, view, this); + + // 2. Create a focus-safe toolbar + m_toolbar = new QtWlPlugin::PluginToolBar(m_fm, this); + m_toolbar->addToolAction("Save", QKeySequence(Qt::CTRL | Qt::Key_S)); + + // 3. Register plugin-specific shortcuts + m_fm->registerShortcut( + QKeySequence(Qt::CTRL | Qt::Key_W), + QtWlPlugin::FocusManager::Always, + [this]() { close(); return true; } + ); + + layout->addWidget(m_toolbar); + layout->addWidget(view); + } + +private: + QtWlPlugin::FocusManager *m_fm; + QtWlPlugin::PluginToolBar *m_toolbar; +}; +``` + +--- + +## Components + +### FocusManager + +**Header:** `` + +The core framework that every plugin using this library needs. It solves the fundamental problem of a Qt widget embedded in a non-Qt host application: focus activation, deactivation, bounce prevention, and keyboard shortcut dispatch. + +#### The Problem It Solves + +When a Qt plugin widget is embedded inside Double Commander (a non-Qt application), focus management becomes complex: + +- Clicking inside the plugin must activate it; clicking outside must deactivate it. +- Programmatic focus changes (e.g. tab switching in DC) must not accidentally activate the plugin. +- Newly added child widgets must not steal focus from the host. +- Keyboard shortcuts must only fire when the plugin is active, and some should be suppressed when the user is editing text in an input field. + +FocusManager handles all of this through a single `qApp` event filter. + +#### Constructor + +```cpp +QtWlPlugin::FocusManager(QWidget *pluginRoot, QWidget *primaryView, QObject *parent = nullptr); +``` + +- `pluginRoot` — the top-level widget of the plugin (typically `this` in the plugin's main widget). +- `primaryView` — the main content widget that should receive focus by default (e.g. a `QTableView`, `QTextEdit`, `QTreeView`). + +The constructor installs the event filter on `qApp` and sets `pluginRoot`'s focus proxy to `primaryView`. + +#### Activation + +| Method | Description | +|--------|-------------| +| `bool isActive() const` | Returns whether the plugin is currently active. | +| `void setActive(bool)` | Manually activate/deactivate. Emits `activated()` or `deactivated()`. When deactivating, clears focus and returns it to the host. | + +**Signals:** `activated()`, `deactivated()` + +Activation is normally handled automatically by the event filter (click inside → activate, click outside → deactivate), but `setActive()` is available for edge cases. + +#### Input Widget Tracking + +The FocusManager distinguishes between "structural" widgets (buttons, headers, etc.) and "input" widgets (text fields, cell editors) to determine when shortcuts should be suppressed. + +| Method | Description | +|--------|-------------| +| `void addInputWidget(QWidget *w)` | Register a widget as an input widget (e.g. a QLineEdit in a find panel). | +| `void removeInputWidget(QWidget *w)` | Unregister an input widget. | +| `bool isInputWidget(QWidget *w) const` | Returns `true` if `w` is registered or is a descendant of `primaryView` (but not `primaryView` itself — this catches dynamically created cell editors). | +| `QWidget *activeInput() const` | Returns the currently focused input widget, or `nullptr`. | + +**Signals:** `inputWidgetEntered(QWidget *w)`, `inputWidgetExited()` + +#### Focus Proxy + +When showing a secondary panel (e.g. find/replace), the plugin's focus proxy should be redirected so keyboard events reach the panel's input fields. + +| Method | Description | +|--------|-------------| +| `void setFocusProxy(QWidget *proxy)` | Redirect `pluginRoot`'s focus proxy to `proxy`. | +| `void resetFocusProxy()` | Reset focus proxy back to `primaryView`. | +| `void restoreViewFocus()` | Explicitly set focus to `primaryView`. | + +#### Shortcut Registration + +Replaces the hardcoded if-chains in `eventFilter` with a declarative registry. + +```cpp +enum ShortcutContext { WhenNoInput, Always }; +using ShortcutId = int; + +ShortcutId registerShortcut(const QKeySequence &keys, ShortcutContext ctx, + std::function handler); +void unregisterShortcut(ShortcutId id); +``` + +- **`WhenNoInput`** — the shortcut only fires when no input widget has focus. Use for navigation keys, copy/paste, delete, etc. +- **`Always`** — the shortcut fires even when the user is editing text. Use for Ctrl+S (save), Ctrl+F (find), Ctrl+W (close), etc. + +The handler returns `true` to consume the event, `false` to let it propagate. + +#### Optional Undo/Redo + +```cpp +void setUndoStack(QUndoStack *stack); +QUndoStack *undoStack() const; +``` + +When a non-null `QUndoStack` is set, FocusManager automatically registers three shortcuts (all with `WhenNoInput` context): + +| Shortcut | Action | +|----------|--------| +| `Ctrl+Z` | `stack->undo()` | +| `Ctrl+Shift+Z` | `stack->redo()` | +| `Ctrl+Y` | `stack->redo()` | + +Calling `setUndoStack(nullptr)` unregisters them. The consumer retains full access to the stack for pushing custom undo commands. + +#### Saved Focus Widget + +```cpp +void saveFocusWidget(QWidget *w); +``` + +Store a reference to the host application's focus widget so the plugin can restore focus to it when deactivating. This is useful when the host tells the plugin which widget had focus before the plugin was activated. + +#### Event Filter Internals + +The event filter handles five event types: + +1. **`MouseButtonPress`** — geometry-based click detection. Click inside plugin → activate. Click outside → deactivate. +2. **`FocusIn`** — tracks which input widget has focus. Ignores programmatic focus changes (`OtherFocusReason`) when inactive. +3. **`KeyPress`** — iterates the shortcut registry, matches key sequences, checks context, dispatches handlers. +4. **`ChildAdded`** — sets `Qt::NoFocus` on newly created child widgets to prevent focus theft. +5. **`QApplication::focusChanged`** — connected in the constructor. Detects focus leaving the plugin hierarchy. When focus enters the plugin while inactive, bounces it back to the host via `QTimer::singleShot(0, ...)`. + +--- + +### PluginToolBar + +**Header:** `` + +A `QToolBar` subclass that integrates with `FocusManager` to prevent focus issues caused by toolbar interaction. + +#### The Problem It Solves + +In a Qt plugin embedded in a non-Qt host, clicking a toolbar button transfers focus to the button widget. Without intervention, this causes the host to think the plugin lost focus and deactivate it. PluginToolBar prevents this by: + +1. Setting `Qt::NoFocus` on all action widgets (buttons, combo boxes, etc.). +2. Restoring focus to `primaryView` after every action trigger via `QTimer::singleShot`. +3. Applying compact default styling. + +#### API + +```cpp +explicit PluginToolBar(FocusManager *fm, QWidget *parent = nullptr); + +QAction *addToolAction(const QString &text, + const QKeySequence &shortcut = {}, + int ctx = 0 /* FocusManager::WhenNoInput */); +``` + +- `addToolAction` creates a `QAction`, adds it to the toolbar, and optionally registers a keyboard shortcut through `FocusManager`. The shortcut text is appended to the tooltip automatically. +- Any dynamically added widgets are also enforced to `NoFocus` via an `actionEvent` override. + +--- + +### EditableGridWidget + +**Header:** `` + +A `QWidget` wrapping a caller-injected `QTableView` with full editing capabilities, undo/redo support, drag-to-move, and context menus. **Format-agnostic** — it provides no file I/O, no parsing, no encoding detection. + +Because `QTableWidget` is a subclass of `QTableView`, you can inject either: +- A `QTableWidget` for simple item-based workflows +- A plain `QTableView` with any `QAbstractItemModel` (e.g. `QStandardItemModel`, `QSqlTableModel`) for model-based workflows + +All internal data access goes through `QAbstractItemModel` / `QModelIndex` — zero `QTableWidgetItem` dependencies inside the grid. + +#### GridMode + +The constructor requires a `GridMode` enum that determines the undo/memory strategy: + +| Mode | Undo Strategy | Sorting | Best For | +|------|--------------|---------|----------| +| `GridMode::MemoryDocument` | Full `QUndoStack` tracking. Sort stores before/after snapshots in RAM. | Snapshot-based undo | `QTableWidget`, `QStandardItemModel`, in-memory data | +| `GridMode::LiveDatabase` | Bypasses `QUndoStack` for data mutations. Sort issues `model->sort()` (SQL `ORDER BY`) with no RAM snapshot. | Direct `model->sort()` | `QSqlTableModel`, transactional models | + +Both modes share context menus, drag-to-reorder, focus management, keyboard shortcuts, and the `WrapAnywhereDelegate`. + +#### Constructor (Dependency Injection) + +```cpp +// The caller creates and configures the view, then hands it to the grid. +explicit EditableGridWidget(QTableView *view, GridMode mode, FocusManager *fm, QWidget *parent = nullptr); +``` + +**In-memory document (full undo):** +```cpp +auto *tw = new QTableWidget(); +auto *grid = new QtWlPlugin::EditableGridWidget(tw, QtWlPlugin::GridMode::MemoryDocument, fm, this); +``` + +**Live database (transactional):** +```cpp +auto *tv = new QTableView(); +tv->setModel(sqlModel); +auto *grid = new QtWlPlugin::EditableGridWidget(tv, QtWlPlugin::GridMode::LiveDatabase, fm, this); +``` + +#### Features + +| Feature | Details | +|---------|---------| +| **Undo/Redo** | `QUndoStack` registered with FocusManager. In `MemoryDocument` mode: four undo command types (`EditCellCommand`, `RowColCommand`, `DataSnapshotCommand`, `SectionMoveCommand`). In `LiveDatabase` mode: data mutations bypass the undo stack — the transactional model handles reverts. | +| **Cell editing** | Enter to open editor, Escape to cancel (reverts to pre-edit value), Up/Down to navigate between cells while editing, Enter to commit and advance right (wraps to next row). | +| **Arrow navigation** | Left at column 0 wraps to the last column of the previous row. Right at last column wraps to column 0 of the next row. | +| **Copy/Paste** | `copySelection(separator)` copies selected cells via `model()->data()`. `pasteSelection()` inserts clipboard rows — wrapped in undo commands in `MemoryDocument`, direct model calls in `LiveDatabase`. | +| **Insert/Delete** | `insertRows()`, `deleteSelectedRows()`, `insertColumns()`, `deleteSelectedColumns()` — undo-wrapped in `MemoryDocument`, direct `model->insertRows()`/`removeRows()` in `LiveDatabase`. | +| **Drag-to-move** | Multi-select row or column drag via headers. Uses a debounce timer to coalesce Qt's per-section `sectionMoved` signals into a single undo command. | +| **Column sorting** | Click header once to arm, click again to sort. `MemoryDocument`: stores full data snapshot for undo. `LiveDatabase`: issues `model->sort()` (SQL `ORDER BY`) with zero RAM overhead. | +| **Word wrap** | `WrapAnywhereDelegate` enables character-level text wrapping (not just word boundaries). Toggle with `setWordWrap(bool)`. | +| **Context menus** | Right-click on the table or vertical header → row operations. Right-click on horizontal header → column operations. Includes insert from clipboard. | +| **Dirty tracking** | `isDirty()` / `dirtyChanged(bool)` signal based on `QUndoStack::isClean()`. | + +#### API + +```cpp +explicit EditableGridWidget(QTableView *view, GridMode mode, FocusManager *fm, QWidget *parent = nullptr); + +// Access +QTableView *view() const; // Returns the injected view (may be QTableView or QTableWidget) +GridMode mode() const; // Returns the active mode +QUndoStack *undoStack() const; + +// Row operations +void copySelection(char separator = '\t'); +QString getSelectionAsText(char separator = '\t'); +void pasteSelection(); +void pasteSelectionAt(int atRow); +void insertRows(int count, int atRow); +void deleteSelectedRows(); + +// Column operations +void copyColumnSelection(char separator = '\t'); +void pasteColumnSelectionAt(int atCol); +void insertColumns(int count, int atCol); +void deleteSelectedColumns(); + +// Appearance +void setWordWrap(bool wrap); +bool wordWrap() const; +void setShowGrid(bool show); + +// State +bool isDirty() const; + +// Signals +void dirtyChanged(bool dirty); +``` + +The consumer provides a `QTableView` (or `QTableWidget`) with a model already set. All grid operations go through `view()->model()` using `QModelIndex`. + +#### Registered Shortcuts + +These are automatically registered with `FocusManager` (all `WhenNoInput`): + +| Shortcut | Action | +|----------|--------| +| `Ctrl+C` | Copy selection as TSV | +| `Ctrl+V` | Paste from clipboard | +| `Delete` | Delete selected rows | +| `Enter` / `Return` | Edit current cell | +| `↑` `↓` `←` `→` | Navigate with right-wrap | +| `Ctrl+Z` | Undo (via FocusManager) | +| `Ctrl+Shift+Z` / `Ctrl+Y` | Redo (via FocusManager) | + +--- + +### FindReplacePanel + +**Header:** `` + +The **base class** for find & replace UI. Provides inputs, match options, and action buttons but has **no scope concept** — scope is entirely the consumer's responsibility. + +#### UI Layout + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Find: [_______________] Replace: [_______________] │ +│ ☐ Match Case ☐ Match Entire Cell ☐ Regex │ +│ [Find Prev] [Find Next] [Replace] [Replace All] status ✕ │ +└──────────────────────────────────────────────────────────────┘ +``` + +#### API + +```cpp +explicit FindReplacePanel(FocusManager *fm, QWidget *parent = nullptr); + +// Configuration +void setReplaceEnabled(bool enabled); // hide replace row for read-only views + +// State +QString findText() const; +QString replaceText() const; +bool matchCase() const; +bool matchEntireCell() const; +bool useRegex() const; + +// Status feedback +void setStatusText(const QString &text); + +// Visibility +void showPanel(bool show); +bool isPanelVisible() const; + +// Signals +void findRequested(bool forward); // true = next, false = previous +void replaceRequested(); +void replaceAllRequested(); +void panelClosed(); +``` + +#### Focus Integration + +- When shown, `setFocusProxy` redirects to the find input and selects all text. +- When hidden, `resetFocusProxy` and `restoreViewFocus` return focus to the primary view. +- Find and replace inputs are registered as input widgets with FocusManager. +- `Ctrl+F` and `Ctrl+R` toggle panel visibility (registered as `Always` shortcuts). + +#### Subclass Extension + +The `optionsRow()` layout is exposed to subclasses for inserting additional widgets: + +```cpp +protected: + QHBoxLayout *optionsRow() const; +``` + +--- + +### ScopedFindReplacePanel + +**Header:** `` + +Extends `FindReplacePanel` with a configurable scope combo box. The consumer provides scope labels and reads the current selection in their signal handlers. + +```cpp +explicit ScopedFindReplacePanel(FocusManager *fm, QWidget *parent = nullptr); + +void setScopes(const QStringList &scopes); // e.g. {"All Cells", "Current Column"} +QString currentScope() const; +``` + +#### Usage + +```cpp +auto *panel = new QtWlPlugin::ScopedFindReplacePanel(m_fm, this); +panel->setScopes({"All Cells", "Selected Cells", "Current Column", "Current Row"}); + +connect(panel, &QtWlPlugin::FindReplacePanel::findRequested, + this, [this, panel](bool forward) { + QString scope = panel->currentScope(); + // ... implement search logic using scope + } +); +``` + +--- + +## Utilities + +### EncodingUtils + +**Header:** `` + +Static utility class for encoding detection (via [enca](https://cihar.com/software/enca/)) and encoding conversion (via GLib's `g_convert_with_fallback`). No instance needed — all methods are static. + +#### API + +```cpp +// Detect encoding of raw bytes. Language hint improves accuracy for +// specific scripts (e.g. "ru" for Russian, "zh" for Chinese). +static QString detectEncoding(const QByteArray &data, const QString &language = {}); + +// Detect encoding from a file (reads up to sampleSize bytes). +static QString detectFileEncoding(const QString &filePath, const QString &language = {}, + int sampleSize = 4096, bool readAll = false); + +// Convert from a detected encoding to UTF-8. +static QByteArray toUtf8(const QByteArray &data, const QString &fromEncoding); + +// Convert a UTF-8 QString to a target encoding. +static QByteArray fromUtf8(const QString &text, const QString &toEncoding); + +// One-shot: detect encoding and decode to QString. +static QString decodeToString(const QByteArray &data, const QString &language = {}); + +// Check runtime availability of enca. +static bool isEncaAvailable(); +``` + +#### Usage + +```cpp +// Detect and decode a file +QFile f("/path/to/file.txt"); +f.open(QFile::ReadOnly); +QByteArray raw = f.readAll(); +f.close(); + +QString encoding = QtWlPlugin::EncodingUtils::detectEncoding(raw, "ru"); +// encoding → "windows-1251" + +QString text = QtWlPlugin::EncodingUtils::decodeToString(raw, "ru"); +// text → decoded UTF-8 QString + +// Convert back for saving +QByteArray encoded = QtWlPlugin::EncodingUtils::fromUtf8(text, "windows-1251"); +``` + +#### Supported Encodings + +Detection accuracy depends on the language hint. enca supports: + +| Language | Code | Scripts | +|----------|------|---------| +| Belarusian | `be` | CP1251, ISO-8859-5, IBM866, KOI8-UNI, etc. | +| Bulgarian | `bg` | CP1251, ISO-8859-5, ECMA-113, etc. | +| Chinese | `zh` | BIG5, GBK, GB2312, HZ, etc. | +| Croatian | `hr` | CP1250, ISO-8859-2, ISO-8859-16, etc. | +| Czech | `cs` | CP1250, ISO-8859-2, IBM852, KEYBCS2, etc. | +| Estonian | `et` | CP1257, ISO-8859-4, ISO-8859-13, etc. | +| Hungarian | `hu` | CP1250, ISO-8859-2, IBM852, etc. | +| Latvian | `lv` | CP1257, ISO-8859-4, ISO-8859-13, etc. | +| Lithuanian | `lt` | CP1257, ISO-8859-4, ISO-8859-13, etc. | +| Polish | `pl` | CP1250, ISO-8859-2, ISO-8859-16, etc. | +| Russian | `ru` | CP1251, KOI8-R, ISO-8859-5, IBM866, etc. | +| Slovak | `sk` | CP1250, ISO-8859-2, IBM852, KEYBCS2, etc. | +| Slovene | `sl` | CP1250, ISO-8859-2, IBM852, etc. | +| Ukrainian | `uk` | CP1251, KOI8-U, ISO-8859-5, IBM866, etc. | + +Use `"__"` or an empty string to let enca auto-detect from the system locale. + +--- + +## Examples + +### Plugin with toolbar + find/replace (no grid) + +```cpp +#include +#include +#include + +class TextViewerWidget : public QWidget { + Q_OBJECT +public: + TextViewerWidget(QWidget *parent = nullptr) : QWidget(parent) { + auto *view = new QTextEdit(this); + auto *layout = new QVBoxLayout(this); + + m_fm = new QtWlPlugin::FocusManager(this, view, this); + + m_toolbar = new QtWlPlugin::PluginToolBar(m_fm, this); + m_toolbar->addToolAction("Print", QKeySequence(Qt::CTRL | Qt::Key_P)); + + m_findReplace = new QtWlPlugin::ScopedFindReplacePanel(m_fm, this); + m_findReplace->setScopes({"Entire Document", "Selection"}); + m_findReplace->setReplaceEnabled(false); // read-only viewer + + connect(m_findReplace, &QtWlPlugin::FindReplacePanel::findRequested, + this, &TextViewerWidget::onFind); + + layout->addWidget(m_toolbar); + layout->addWidget(view, 1); + layout->addWidget(m_findReplace); + } + +private slots: + void onFind(bool forward) { + QString query = m_findReplace->findText(); + QString scope = m_findReplace->currentScope(); + // ... implement search + } + +private: + QtWlPlugin::FocusManager *m_fm; + QtWlPlugin::PluginToolBar *m_toolbar; + QtWlPlugin::ScopedFindReplacePanel *m_findReplace; +}; +``` + +### Plugin with editable grid + undo (QTableWidget) + +```cpp +#include +#include +#include +#include + +class DataEditorWidget : public QWidget { + Q_OBJECT +public: + DataEditorWidget(QWidget *parent = nullptr) : QWidget(parent) { + auto *layout = new QVBoxLayout(this); + + // Inject a QTableWidget for item-based convenience + auto *table = new QTableWidget(); + m_fm = new QtWlPlugin::FocusManager(this, table, this); + m_grid = new QtWlPlugin::EditableGridWidget( + table, QtWlPlugin::GridMode::MemoryDocument, m_fm, this); + + m_toolbar = new QtWlPlugin::PluginToolBar(m_fm, this); + auto *actSave = m_toolbar->addToolAction("Save", + QKeySequence(Qt::CTRL | Qt::Key_S), + QtWlPlugin::FocusManager::Always); + connect(actSave, &QAction::triggered, this, &DataEditorWidget::onSave); + + connect(m_grid, &QtWlPlugin::EditableGridWidget::dirtyChanged, + this, [this](bool dirty) { + setWindowTitle(dirty ? "Data Editor *" : "Data Editor"); + }); + + layout->addWidget(m_toolbar); + layout->addWidget(m_grid, 1); + loadData(); + } + +private: + void loadData() { + // You can still access QTableWidget-specific methods on the original pointer + auto *tw = qobject_cast(m_grid->view()); + tw->setRowCount(100); + tw->setColumnCount(5); + // ... populate cells with QTableWidgetItem + } + + void onSave() { + // ... write data in your format + m_fm->undoStack()->setClean(); + } + + QtWlPlugin::FocusManager *m_fm; + QtWlPlugin::PluginToolBar *m_toolbar; + QtWlPlugin::EditableGridWidget *m_grid; +}; +``` + +### Plugin with editable grid + SQL model (QTableView) + +```cpp +#include +#include +#include +#include +#include + +class SqlEditorWidget : public QWidget { + Q_OBJECT +public: + SqlEditorWidget(QSqlDatabase db, QWidget *parent = nullptr) : QWidget(parent) { + auto *layout = new QVBoxLayout(this); + + // Inject a QTableView with a SQL model + auto *sqlView = new QTableView(); + auto *sqlModel = new QSqlTableModel(this, db); + sqlModel->setTable("users"); + sqlModel->select(); + sqlView->setModel(sqlModel); + + m_fm = new QtWlPlugin::FocusManager(this, sqlView, this); + m_grid = new QtWlPlugin::EditableGridWidget( + sqlView, QtWlPlugin::GridMode::LiveDatabase, m_fm, this); + + // The grid handles all focus, context menus, and shortcuts. + // Sorting uses model->sort() → SQL ORDER BY (no RAM snapshot). + // Insert/delete call model directly (no undo stack wrapping). + // model->setData() triggers SQL UPDATEs via QSqlTableModel. + + layout->addWidget(m_grid, 1); + } + +private: + QtWlPlugin::FocusManager *m_fm; + QtWlPlugin::EditableGridWidget *m_grid; +}; +``` + +### Encoding detection on file load + +```cpp +#include + +void MyPlugin::loadFile(const QString &path) { + QFile file(path); + file.open(QFile::ReadOnly); + QByteArray raw = file.readAll(); + file.close(); + + QString encoding = QtWlPlugin::EncodingUtils::detectEncoding(raw); + if (encoding.isEmpty()) + encoding = "UTF-8"; // fallback + + QByteArray utf8 = QtWlPlugin::EncodingUtils::toUtf8(raw, encoding); + QString content = QString::fromUtf8(utf8); + + m_detectedEncoding = encoding; // store for save + // ... display content +} + +void MyPlugin::saveFile(const QString &path) { + QString content = /* ... get content ... */; + QByteArray encoded = QtWlPlugin::EncodingUtils::fromUtf8(content, m_detectedEncoding); + + QFile file(path); + file.open(QFile::WriteOnly); + file.write(encoded); + file.close(); +} +``` + +--- + +## Design Decisions + +### Shortcut Registry over Hardcoded eventFilter + +Existing plugins (`csvview`, `logview`, `kate`) each contain near-identical `eventFilter` implementations with large if-chains mapping key combinations to actions. This library replaces that pattern with a declarative `registerShortcut()` API. New plugins never need to subclass or override `eventFilter` for keyboard shortcuts. + +### Static Library + +Each consumer plugin links `libwayland_qt_base.a` and produces a self-contained `.wlx` with no runtime dependency on a separate shared object. This simplifies deployment — just ship the `.wlx` file. + +### Optional Undo + +Not all plugins need undo/redo (e.g. a read-only log viewer). Calling `setUndoStack()` is optional. When set, undo shortcuts are auto-registered; when cleared, they're auto-unregistered. The consumer always has direct access to the `QUndoStack` for pushing custom commands, checking `isClean()`, etc. + +### FindReplacePanel Hierarchy + +The base class `FindReplacePanel` has zero scope awareness. This is intentional — a text editor plugin's "scope" concept (selection, whole file) is fundamentally different from a grid plugin's (all cells, current column, current row). The base class provides the UI and signals; the consumer implements matching. + +`ScopedFindReplacePanel` adds a scope combo box for plugins that want predefined scope options. It inserts into the base's `optionsRow()` layout, so there's no UI duplication. + +### Model-Based, Format-Agnostic Grid + +`EditableGridWidget` accepts a `QTableView*` via dependency injection and performs all data operations through `QAbstractItemModel` (`model()->data()`, `model()->setData()`, `model()->insertRows()`, etc.). This means the same grid code works identically whether the underlying model is a `QTableWidget`'s internal model, a `QStandardItemModel`, a `QSqlTableModel`, or any custom `QAbstractItemModel`. The consumer is responsible for file I/O, encoding, and format-specific quoting — the grid knows nothing about data formats. + +### GridMode: MemoryDocument vs LiveDatabase + +The `GridMode` enum forces the downstream developer to explicitly acknowledge the memory paradigm of the data they are loading. `MemoryDocument` enables full undo/redo tracking — every edit, insert, delete, and sort is wrapped in a `QUndoCommand` backed by in-memory data snapshots. `LiveDatabase` bypasses all memory-intensive operations: sorting delegates to `model->sort()` (which translates to SQL `ORDER BY` for `QSqlTableModel`), and structural mutations (insert/delete) call the model directly without undo wrappers. This prevents the catastrophic RAM explosion that would occur if a 5-million-row SQLite table were snapshotted into `QVariantList` objects for undo support. Both modes share identical context menus, drag-to-reorder, focus proxying, keyboard shortcuts, and the `WrapAnywhereDelegate`. + +### enca as Build Dependency + +The encoding detection library [enca](https://cihar.com/software/enca/) is downloaded and statically compiled during the CMake configure step. This mirrors the pattern used by `csvview` and avoids requiring enca to be installed system-wide. The static link means no runtime dependency. + +--- + +## Directory Layout + +``` +wlx/wlxbase_wlqt/ +├── CMakeLists.txt +├── README.md +├── include/ +│ └── wlxbase_wlqt/ +│ ├── EditableGridWidget.h +│ ├── EncodingUtils.h +│ ├── FindReplacePanel.h +│ ├── FocusManager.h +│ ├── PluginToolBar.h +│ └── ScopedFindReplacePanel.h +└── src/ + ├── EditableGridWidget.cpp + ├── EncodingUtils.cpp + ├── FindReplacePanel.cpp + ├── FocusManager.cpp + ├── PluginToolBar.cpp + └── ScopedFindReplacePanel.cpp +``` diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/CrashLogger.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/CrashLogger.h new file mode 100644 index 0000000..8ed40c4 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/CrashLogger.h @@ -0,0 +1,74 @@ +#pragma once + +/// @file CrashLogger.h +/// Global exception logging for WLX plugins. +/// +/// Usage in wlx_entry.cpp: +/// @code +/// #include +/// +/// HWND DCPCALL ListLoad(HWND ParentWin, char* FileToLoad, int ShowFlags) +/// { +/// WLX_TRY { +/// // ... normal plugin code ... +/// } WLX_CATCH("ListLoad"); +/// return nullptr; // fallback return on exception +/// } +/// @endcode +/// +/// Logs are written to .log next to the .wlx file. + +#include +#include + +namespace QtWlPlugin { + +class CrashLogger { +public: + /// Initialize with any address inside the plugin .so/.wlx + /// (typically a function pointer like &ListLoad). + /// Determines the plugin path and creates the log file path. + static void init(void *addressInPlugin); + + /// Log a caught std::exception with stack trace. + static void log(const char *entryPoint, const std::exception &e); + + /// Log an unknown exception (catch ...) with stack trace. + static void logUnknown(const char *entryPoint); + + /// Log a custom message (for warnings, state dumps, etc.) + static void logMessage(const char *entryPoint, const char *message); + + /// Get the log file path (empty if not initialized). + static const std::string &logPath(); + +private: + static void writeEntry(const char *entryPoint, const char *type, + const char *message); +}; + +} // namespace QtWlPlugin + +/// Convenience macros for wrapping WLX entry points. +/// +/// WLX_TRY { ... } WLX_CATCH("FunctionName"); +/// +/// On first use, auto-initializes the logger with the current function address. +#define WLX_TRY \ + { \ + static bool _wlx_logger_init = false; \ + if (!_wlx_logger_init) { \ + _wlx_logger_init = true; \ + QtWlPlugin::CrashLogger::init( \ + reinterpret_cast(&_wlx_logger_init)); \ + } \ + } \ + try + +#define WLX_CATCH(funcName) \ + catch (const std::exception &_wlx_ex) { \ + QtWlPlugin::CrashLogger::log(funcName, _wlx_ex); \ + } \ + catch (...) { \ + QtWlPlugin::CrashLogger::logUnknown(funcName); \ + } diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EditableGridWidget.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EditableGridWidget.h new file mode 100644 index 0000000..a7ddb46 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EditableGridWidget.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +class FocusManager; + +/// Defines the memory and undo strategy for EditableGridWidget. +/// +/// MemoryDocument: Full QUndoStack tracking for all data mutations. +/// Best for QTableWidget, QStandardItemModel, or any in-memory model. +/// Sorting snapshots entire table state for undo. +/// +/// LiveDatabase: Bypasses QUndoStack for data mutations (insert, delete, sort). +/// Best for QSqlTableModel or other transactional models. +/// Sorting delegates to model->sort() (SQL ORDER BY) with no RAM snapshot. +/// Copy, context menus, drag-to-reorder, focus, and shortcuts still work. +enum class GridMode { + MemoryDocument, + LiveDatabase +}; + +/// Custom delegate that wraps text at any character (not just word boundaries). +class WrapAnywhereDelegate : public QStyledItemDelegate { +public: + using QStyledItemDelegate::QStyledItemDelegate; + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + void setWrapAnywhere(bool wrap); + bool wrapAnywhere() const; + +private: + bool m_wrap = false; +}; + +/// A QTableView wrapper with full undo/redo, keyboard navigation, +/// drag-to-move columns/rows, context menus, and editing support. +/// +/// Accepts a pre-instantiated QTableView* and a GridMode via dependency injection. +/// Because QTableWidget is a subclass of QTableView, you can inject either: +/// +/// // Item-based (in-memory, full undo): +/// auto *grid = new EditableGridWidget(new QTableWidget(), GridMode::MemoryDocument, fm, this); +/// +/// // Database (transactional, no RAM snapshots): +/// auto *sqlView = new QTableView(); +/// sqlView->setModel(sqlModel); +/// auto *grid = new EditableGridWidget(sqlView, GridMode::LiveDatabase, fm, this); +/// +/// All data access goes through QAbstractItemModel — no QTableWidgetItem +/// dependency in the grid's own logic. +class EditableGridWidget : public QWidget { + Q_OBJECT +public: + /// Takes ownership of the view and parents it to this widget. + explicit EditableGridWidget(QTableView *view, GridMode mode, FocusManager *fm, QWidget *parent = nullptr); + + /// Access the underlying view (may be QTableView or QTableWidget). + QTableView *view() const; + GridMode mode() const; + QUndoStack *undoStack() const; + + // --- Data operations (format-agnostic, model-based) --- + void copySelection(char separator = '\t'); + QString getSelectionAsText(char separator = '\t'); + void pasteSelection(); + void pasteSelectionAt(int atRow); + void insertRows(int count, int atRow); + void deleteSelectedRows(); + + void copyColumnSelection(char separator = '\t'); + void pasteColumnSelectionAt(int atCol); + void insertColumns(int count, int atCol); + void deleteSelectedColumns(); + + // --- Appearance --- + void setWordWrap(bool wrap); + bool wordWrap() const; + void setShowGrid(bool show); + + // --- Context menu integration --- + /// Set the filter row widget for Filters toggle in context menu. + void setFilterRow(class FilterRowWidget *filterRow); + + + + /// Register additional context menu entries. + /// The callback receives the QMenu and the clicked QModelIndex. + void setExtraContextMenuCallback( + std::function callback); + + // --- State --- + bool isDirty() const; + +signals: + void dirtyChanged(bool dirty); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +private: + void setupView(); + void registerShortcuts(); + void setupDragToMove(); + void showRowContextMenu(const QPoint &pos); + void showColumnContextMenu(const QPoint &pos); + void onSortByColumn(int column); + void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, + const QList &roles); + void updateRowNumbers(); + bool isSectionSelected(QHeaderView *header, int logicalIndex) const; + + /// Helper: row/column count via the model + int rowCount() const; + int colCount() const; + + FocusManager *m_fm; + QTableView *m_view; + GridMode m_mode; + QUndoStack *m_undoStack; + WrapAnywhereDelegate *m_wrapDelegate; + bool m_isProgrammaticChange; + + // Sort state + int m_lastSortColumn; + Qt::SortOrder m_lastSortOrder; + + // Drag-to-move state + QHeaderView *m_dragHeader; + int m_dragLogicalIndex; + QList m_dragBeforeOrder; + QSet m_dragSelectedSections; + bool m_isDraggingSection; + QTimer *m_moveDebounceTimer; + + // Context menu integrations + FilterRowWidget *m_filterRow = nullptr; + + std::function m_extraMenuCallback; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EncodingUtils.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EncodingUtils.h new file mode 100644 index 0000000..880140a --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EncodingUtils.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace QtWlPlugin { + +/// Encoding detection and conversion utility for plugin developers. +/// +/// Wraps the enca library for detection and glib for encoding conversion. +/// All methods are static — no instance needed. +class EncodingUtils { +public: + /// Detect the encoding of raw byte data. + /// @param data Raw bytes to analyze + /// @param language 2-letter ISO language code hint for enca (e.g. "ru", "en", "de"). + /// If empty, derived from the current locale. + /// @return Detected encoding name (e.g. "UTF-8", "windows-1251") or empty on failure. + static QString detectEncoding(const QByteArray &data, const QString &language = {}); + + /// Detect encoding from a file. Reads up to sampleSize bytes by default. + static QString detectFileEncoding(const QString &filePath, const QString &language = {}, + int sampleSize = 4096, bool readAll = false); + + /// Convert byte data from one encoding to UTF-8. + /// Returns the original data if conversion fails. + static QByteArray toUtf8(const QByteArray &data, const QString &fromEncoding); + + /// Convert a UTF-8 QString to a target encoding. + /// Returns UTF-8 bytes if conversion fails. + static QByteArray fromUtf8(const QString &text, const QString &toEncoding); + + /// Detect encoding and decode to QString in one call. + static QString decodeToString(const QByteArray &data, const QString &language = {}); + + /// Check if enca support is available at runtime. + static bool isEncaAvailable(); +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterRowWidget.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterRowWidget.h new file mode 100644 index 0000000..fb59b70 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterRowWidget.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +class QLineEdit; +class QHBoxLayout; +class QTableView; + +namespace QtWlPlugin { + +/// A row of QLineEdit widgets for per-column filtering of a QTableView. +/// +/// Sits between the column headers and the data. Each column gets its own +/// filter input. Widths sync with column widths automatically. +/// +/// Usage: +/// auto *filter = new FilterRowWidget(tableView, this); +/// connect(filter, &FilterRowWidget::filterChanged, ...); +class FilterRowWidget : public QWidget { + Q_OBJECT +public: + explicit FilterRowWidget(QTableView *view, QWidget *parent = nullptr); + + void setFilterVisible(bool visible); + bool isFilterVisible() const; + void clearFilters(); + + /// Rebuild filter inputs when the model/columns change. + void syncToModel(); + +signals: + void filterChanged(int column, const QString &text); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +private: + void rebuildInputs(); + void syncWidths(); + + QTableView *m_view; + QHBoxLayout *m_layout; + QVector m_inputs; + bool m_visible = true; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterableHeaderView.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterableHeaderView.h new file mode 100644 index 0000000..38fb37c --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterableHeaderView.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +class QLineEdit; + +namespace QtWlPlugin { + +/// QHeaderView with an optional embedded filter row below the column labels. +/// +/// When filtering is enabled, QLineEdit inputs appear directly under each +/// column header, pixel-aligned with the sections (no spacer hacks needed). +/// +/// Usage: +/// auto *header = new FilterableHeaderView(Qt::Horizontal, tableView); +/// header->setFilterEnabled(true); +/// tableView->setHorizontalHeader(header); +/// connect(header, &FilterableHeaderView::filterChanged, ...); +class FilterableHeaderView : public QHeaderView { + Q_OBJECT +public: + explicit FilterableHeaderView(Qt::Orientation orientation, QWidget *parent = nullptr); + + /// Enable or disable the filter row. + void setFilterEnabled(bool enabled); + bool isFilterEnabled() const; + + /// Enable or disable the header labels (if false, the header collapses to only show the filters). + void setHeaderVisible(bool visible); + bool isHeaderVisible() const; + + /// Clear all filter inputs. + void clearFilters(); + + /// Get the current filter text for a column. + QString filterText(int column) const; + + QSize sizeHint() const override; + +signals: + /// Emitted when the user types in a column's filter input. + void filterChanged(int column, const QString &text); + +protected: + void updateGeometries() override; + void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const override; + +private slots: + void adjustInputPositions(); + +private: + void rebuildInputs(); + + bool m_filterEnabled = false; + bool m_headerVisible = true; + int m_filterRowHeight = 24; + QVector m_inputs; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FindReplacePanel.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FindReplacePanel.h new file mode 100644 index 0000000..1f3f487 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FindReplacePanel.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +class QLineEdit; +class QCheckBox; +class QLabel; +class QPushButton; +class QHBoxLayout; +class QVBoxLayout; + +namespace QtWlPlugin { + +class FocusManager; + +/// Base find & replace panel with no scope concept. +/// +/// Provides the UI shell (find/replace inputs, match options, action buttons) +/// and emits signals. The consumer implements the actual matching logic by +/// connecting to findRequested, replaceRequested, and replaceAllRequested. +/// +/// Scope is entirely the consumer's responsibility at this level. +/// For built-in scope support, use ScopedFindReplacePanel instead. +class FindReplacePanel : public QWidget { + Q_OBJECT +public: + explicit FindReplacePanel(FocusManager *fm, QWidget *parent = nullptr); + + // --- Configuration --- + void setReplaceEnabled(bool enabled); + + // --- State --- + QString findText() const; + QString replaceText() const; + bool matchCase() const; + bool matchEntireCell() const; + bool useRegex() const; + + // --- Status feedback --- + void setStatusText(const QString &text); + + // --- Visibility --- + void showPanel(bool show); + bool isPanelVisible() const; + + // --- Access for subclasses --- + FocusManager *focusManager() const; + QLabel *statusLabel() const; + +signals: + void findRequested(bool forward); + void replaceRequested(); + void replaceAllRequested(); + void panelClosed(); + +protected: + /// Hook for subclasses to insert widgets into the options row. + QHBoxLayout *optionsRow() const; + +private: + FocusManager *m_fm; + QLineEdit *m_txtFind; + QLineEdit *m_txtReplace; + QLabel *m_lblReplace; + QCheckBox *m_chkMatchCase; + QCheckBox *m_chkMatchEntire; + QCheckBox *m_chkRegex; + QLabel *m_lblStatus; + QHBoxLayout *m_optionsRow; + QPushButton *m_btnReplace; + QPushButton *m_btnReplaceAll; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FocusManager.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FocusManager.h new file mode 100644 index 0000000..1bdf3c2 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FocusManager.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class QUndoStack; + +namespace QtWlPlugin { + +/// Core focus management framework for Qt WLX plugins embedded in a host application. +/// +/// Manages the entire focus lifecycle: activation/deactivation on click, +/// focus bounce prevention, input widget tracking, and a shortcut registry +/// that replaces the hardcoded eventFilter if-chains found in existing plugins. +class FocusManager : public QObject { + Q_OBJECT +public: + explicit FocusManager(QWidget *pluginRoot, QWidget *primaryView, QObject *parent = nullptr); + ~FocusManager() override; + + // --- Activation --- + bool isActive() const; + void setActive(bool active); + + // --- Input widget tracking --- + void addInputWidget(QWidget *w); + void removeInputWidget(QWidget *w); + bool isInputWidget(QWidget *w) const; + QWidget *activeInput() const; + + // --- Focus proxy management --- + void setFocusProxy(QWidget *proxy); + void resetFocusProxy(); + + // --- Shortcut registration --- + enum ShortcutContext { WhenNoInput, Always }; + using ShortcutId = int; + ShortcutId registerShortcut(const QKeySequence &keys, ShortcutContext ctx, + std::function handler); + void unregisterShortcut(ShortcutId id); + + // --- Optional undo/redo --- + /// When set, auto-registers Ctrl+Z (undo), Ctrl+Shift+Z (redo), Ctrl+Y (redo). + /// The consumer retains full access to the stack for custom manipulation. + void setUndoStack(QUndoStack *stack); + QUndoStack *undoStack() const; + + // --- Saved focus (for restoring to host app) --- + void saveFocusWidget(QWidget *w); + + // --- Access --- + QWidget *pluginRoot() const; + QWidget *primaryView() const; + + // --- Focus restoration --- + void restoreViewFocus(); + static void expectReloadFocus(); + +signals: + void activated(); + void deactivated(); + void inputWidgetEntered(QWidget *w); + void inputWidgetExited(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +private: + void installFocusGuard(); + void restoreFocusToDC(); + + QPointer m_pluginRoot; + QPointer m_primaryView; + bool m_isActive; + QPointer m_savedFocusWidget; + QPointer m_activeInput; + QSet m_extraInputWidgets; + + QUndoStack *m_undoStack; + QVector m_undoShortcutIds; + + struct RegisteredShortcut { + ShortcutId id; + QKeySequence keys; + ShortcutContext ctx; + std::function handler; + }; + QVector m_shortcuts; + ShortcutId m_nextShortcutId; + static bool s_reloadFocusTarget; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginSplitView.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginSplitView.h new file mode 100644 index 0000000..504c73a --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginSplitView.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace QtWlPlugin { + +/// Reusable left-panel + right-content splitter. +/// +/// Both structview (tree) and dbview (table list) use this pattern. +/// Sets sensible defaults: left panel 180px, stretch on right. +class PluginSplitView : public QSplitter { + Q_OBJECT +public: + explicit PluginSplitView(QWidget *leftPanel, QWidget *rightContent, + QWidget *parent = nullptr); + + void setLeftWidth(int pixels); + QWidget *leftPanel() const; + QWidget *rightContent() const; + +private: + QWidget *m_left; + QWidget *m_right; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginStatusBar.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginStatusBar.h new file mode 100644 index 0000000..9d460e6 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginStatusBar.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class QHBoxLayout; + +namespace QtWlPlugin { + +/// A compact status bar for WLX plugins. +/// +/// Displays configurable sections separated by vertical lines. +/// Typical content: encoding, format name, row count, extra info. +/// +/// Example: | UTF-8 | JSON | Rows: 4/4 | +/// Example: | Tables: 11 | Views: 0 | SQLite | Rows: 3/59 | +class PluginStatusBar : public QWidget { + Q_OBJECT +public: + explicit PluginStatusBar(QWidget *parent = nullptr); + + void setEncoding(const QString &encoding); + void setFormatInfo(const QString &info); + void setRowCount(int filtered, int total); + void setExtraInfo(const QString &key, const QString &value); + void removeExtraInfo(const QString &key); + +private: + void rebuild(); + QFrame *createSeparator(); + + QLabel *m_encodingLabel; + QLabel *m_formatLabel; + QLabel *m_rowLabel; + QMap m_extras; + QHBoxLayout *m_layout; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginToolBar.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginToolBar.h new file mode 100644 index 0000000..044295e --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginToolBar.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace QtWlPlugin { + +class FocusManager; + +/// A QToolBar subclass that automatically integrates with FocusManager. +/// +/// All action widgets are set to Qt::NoFocus, and focus is restored to the +/// primary view after any action trigger. Provides convenience for adding +/// actions with automatic shortcut registration through FocusManager. +class PluginToolBar : public QToolBar { + Q_OBJECT +public: + explicit PluginToolBar(FocusManager *fm, QWidget *parent = nullptr); + + /// Add an action and optionally register a shortcut through FocusManager. + QAction *addToolAction(const QString &text, + const QKeySequence &shortcut = {}, + int ctx = 0 /* FocusManager::WhenNoInput */); + +protected: + void actionEvent(QActionEvent *event) override; + +private: + void enforceNoFocus(); + FocusManager *m_fm; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ScopedFindReplacePanel.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ScopedFindReplacePanel.h new file mode 100644 index 0000000..72eb137 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ScopedFindReplacePanel.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +class QComboBox; + +namespace QtWlPlugin { + +/// FindReplacePanel subclass that adds configurable scope options via a QComboBox. +/// +/// The consumer sets scope labels (e.g. "All Cells", "Current Column") via setScopes(), +/// then reads currentScope() in their signal handlers to filter the search. +class ScopedFindReplacePanel : public FindReplacePanel { + Q_OBJECT +public: + explicit ScopedFindReplacePanel(FocusManager *fm, QWidget *parent = nullptr); + + void setScopes(const QStringList &scopes); + QString currentScope() const; + +private: + QComboBox *m_comboScope; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/SequentialRowProxyModel.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/SequentialRowProxyModel.h new file mode 100644 index 0000000..1e15078 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/SequentialRowProxyModel.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace QtWlPlugin { + +class SequentialRowProxyModel : public QSortFilterProxyModel { + Q_OBJECT +public: + explicit SequentialRowProxyModel(QObject *parent = nullptr); + ~SequentialRowProxyModel() override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ThemeManager.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ThemeManager.h new file mode 100644 index 0000000..0f03c2f --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ThemeManager.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace QtWlPlugin { + +/// Shared dark/light theme management for WLX plugins. +/// +/// Detects the system or Double Commander theme by inspecting the +/// application palette and applies a comprehensive Qt stylesheet. +/// Falls back to dark mode when detection is inconclusive. +class ThemeManager { +public: + enum Theme { Light, Dark }; + + /// Detect system/DC dark vs light theme from the application palette. + /// Falls back to Dark when undetermined. + static Theme detectSystemTheme(); + + static void applyTheme(QWidget *root, Theme theme); + static bool isDark(); + +private: + static QString darkStylesheet(); + static Theme s_current; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/CrashLogger.cpp b/wlx/wlxbase_wlqt/src/CrashLogger.cpp new file mode 100644 index 0000000..f14665c --- /dev/null +++ b/wlx/wlxbase_wlqt/src/CrashLogger.cpp @@ -0,0 +1,193 @@ +#include + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#include +#else +#include +#endif + +namespace QtWlPlugin { + +static std::string s_logPath; +static std::string s_pluginName; +static std::mutex s_mutex; + +void CrashLogger::init(void *addressInPlugin) +{ + if (!s_logPath.empty()) + return; // already initialized + +#ifndef _WIN32 + Dl_info info; + if (dladdr(addressInPlugin, &info) && info.dli_fname) { + // Plugin path: /path/to/build/myplugin_qt6.wlx + std::string pluginPath(info.dli_fname); + + // Extract directory + char *pathCopy = strdup(pluginPath.c_str()); + std::string dir(dirname(pathCopy)); + free(pathCopy); + + // Extract plugin name (without extension) + char *baseCopy = strdup(pluginPath.c_str()); + std::string base(basename(baseCopy)); + free(baseCopy); + + auto dot = base.rfind('.'); + if (dot != std::string::npos) + base = base.substr(0, dot); + + s_pluginName = base; + s_logPath = dir + "/" + base + ".log"; + } +#else + HMODULE hModule = nullptr; + GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(addressInPlugin), &hModule); + if (hModule) { + char path[MAX_PATH]; + GetModuleFileNameA(hModule, path, MAX_PATH); + std::string pluginPath(path); + + auto lastSlash = pluginPath.rfind('\\'); + std::string dir = (lastSlash != std::string::npos) ? pluginPath.substr(0, lastSlash) : "."; + std::string base = (lastSlash != std::string::npos) ? pluginPath.substr(lastSlash + 1) : pluginPath; + + auto dot = base.rfind('.'); + if (dot != std::string::npos) + base = base.substr(0, dot); + + s_pluginName = base; + s_logPath = dir + "\\" + base + ".log"; + } +#endif +} + +const std::string &CrashLogger::logPath() +{ + return s_logPath; +} + +/// Format a backtrace into the log. +static std::string captureBacktrace() +{ + std::string result; +#ifndef _WIN32 + void *frames[64]; + int count = backtrace(frames, 64); + char **symbols = backtrace_symbols(frames, count); + + if (symbols) { + // Skip first 3 frames (captureBacktrace, writeEntry, log/logUnknown) + for (int i = 3; i < count; ++i) { + result += " "; + + // Try to demangle the symbol + // Format: "./libfoo.so(+0x1234) [0x7fff1234]" + // or: "./libfoo.so(_ZN3Foo3barEv+0x42) [0x7fff1234]" + std::string sym(symbols[i]); + auto lparen = sym.find('('); + auto plus = sym.find('+', lparen != std::string::npos ? lparen : 0); + auto rparen = sym.find(')', lparen != std::string::npos ? lparen : 0); + + if (lparen != std::string::npos && rparen != std::string::npos + && plus != std::string::npos && plus > lparen + 1) { + std::string mangled = sym.substr(lparen + 1, plus - lparen - 1); + int status = -1; + char *demangled = abi::__cxa_demangle(mangled.c_str(), nullptr, nullptr, &status); + if (status == 0 && demangled) { + result += sym.substr(0, lparen + 1); + result += demangled; + result += sym.substr(plus); + free(demangled); + } else { + result += sym; + } + } else { + result += sym; + } + result += "\n"; + } + free(symbols); + } +#endif + return result; +} + +void CrashLogger::writeEntry(const char *entryPoint, const char *type, + const char *message) +{ + if (s_logPath.empty()) + return; + + std::lock_guard lock(s_mutex); + + FILE *f = fopen(s_logPath.c_str(), "a"); + if (!f) + return; + + // Timestamp + time_t now = time(nullptr); + struct tm tm; + localtime_r(&now, &tm); + char timebuf[64]; + strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", &tm); + + fprintf(f, "--- %s [%s] %s ---\n", timebuf, s_pluginName.c_str(), entryPoint); + fprintf(f, " Type: %s\n", type); + if (message && message[0]) + fprintf(f, " Message: %s\n", message); + + // Stack trace + std::string bt = captureBacktrace(); + if (!bt.empty()) { + fprintf(f, " Stack trace:\n%s", bt.c_str()); + } + + fprintf(f, "\n"); + fclose(f); + + // Also print to stderr so it shows in console + fprintf(stderr, "[%s] EXCEPTION in %s: %s: %s\n", + s_pluginName.c_str(), entryPoint, type, + (message && message[0]) ? message : "(no message)"); +} + +void CrashLogger::log(const char *entryPoint, const std::exception &e) +{ + // Try to get the demangled exception type name + const char *typeName = "std::exception"; +#ifndef _WIN32 + int status = -1; + char *demangled = abi::__cxa_demangle(typeid(e).name(), nullptr, nullptr, &status); + if (status == 0 && demangled) { + writeEntry(entryPoint, demangled, e.what()); + free(demangled); + return; + } +#endif + typeName = typeid(e).name(); + writeEntry(entryPoint, typeName, e.what()); +} + +void CrashLogger::logUnknown(const char *entryPoint) +{ + writeEntry(entryPoint, "unknown (catch ...)", "Non-std::exception object thrown"); +} + +void CrashLogger::logMessage(const char *entryPoint, const char *message) +{ + writeEntry(entryPoint, "info", message); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/EditableGridWidget.cpp b/wlx/wlxbase_wlqt/src/EditableGridWidget.cpp new file mode 100644 index 0000000..f6ee369 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/EditableGridWidget.cpp @@ -0,0 +1,1206 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +// --------------------------------------------------------------------------- +// Undo Commands — all operate through QAbstractItemModel, not QTableWidgetItem +// --------------------------------------------------------------------------- + +class EditCellCommand : public QUndoCommand { +public: + EditCellCommand(QAbstractItemModel *model, const QModelIndex &index, + const QVariant &oldValue, const QVariant &newValue, + QUndoCommand *parent = nullptr) + : QUndoCommand(parent), m_model(model), + m_row(index.row()), m_col(index.column()), + m_oldValue(oldValue), m_newValue(newValue) { + setText(QString("Edit cell (%1, %2)").arg(m_row).arg(m_col)); + } + void undo() override { + QModelIndex idx = m_model->index(m_row, m_col); + m_model->setData(idx, m_oldValue, Qt::EditRole); + } + void redo() override { + QModelIndex idx = m_model->index(m_row, m_col); + m_model->setData(idx, m_newValue, Qt::EditRole); + } +private: + QAbstractItemModel *m_model; + int m_row, m_col; + QVariant m_oldValue, m_newValue; +}; + +class RowColCommand : public QUndoCommand { +public: + RowColCommand(QAbstractItemModel *model, int index, int count, bool isRow, bool isInsert, + QUndoCommand *parent = nullptr) + : QUndoCommand(parent), m_model(model), m_index(index), m_count(count), + m_isRow(isRow), m_isInsert(isInsert) { + setText(QString("%1 %2 %3(s)").arg(isInsert ? "Insert" : "Delete") + .arg(count).arg(isRow ? "row" : "col")); + if (!isInsert) { + // Snapshot data before deletion for undo restore + int crossDim = isRow ? model->columnCount() : model->rowCount(); + for (int i = 0; i < count; ++i) { + QVariantList list; + for (int j = 0; j < crossDim; ++j) { + QModelIndex idx = isRow ? model->index(index + i, j) + : model->index(j, index + i); + list << model->data(idx, Qt::EditRole); + } + m_data << list; + } + } + } + void undo() override { if (m_isInsert) applyDelete(); else applyInsert(); } + void redo() override { if (m_isInsert) applyInsert(); else applyDelete(); } + +private: + void applyInsert() { + if (m_isRow) m_model->insertRows(m_index, m_count); + else m_model->insertColumns(m_index, m_count); + + // Restore saved data if we have any (undo of delete) + if (!m_data.isEmpty()) { + int crossDim = m_isRow ? m_model->columnCount() : m_model->rowCount(); + for (int i = 0; i < m_count && i < m_data.size(); ++i) { + const QVariantList &list = m_data[i]; + for (int j = 0; j < crossDim && j < list.size(); ++j) { + QModelIndex idx = m_isRow ? m_model->index(m_index + i, j) + : m_model->index(j, m_index + i); + m_model->setData(idx, list[j], Qt::EditRole); + } + } + } + } + void applyDelete() { + if (m_isRow) m_model->removeRows(m_index, m_count); + else m_model->removeColumns(m_index, m_count); + } + QAbstractItemModel *m_model; + int m_index, m_count; + QList m_data; + bool m_isRow, m_isInsert; +}; + +class DataSnapshotCommand : public QUndoCommand { +public: + DataSnapshotCommand(QAbstractItemModel *model, const QList &before, + const QList &after, const QString &text) + : m_model(model), m_before(before), m_after(after), m_first(true) { setText(text); } + void undo() override { restore(m_before); } + void redo() override { if (m_first) { m_first = false; return; } restore(m_after); } +private: + void restore(const QList &data) { + for (int r = 0; r < data.size() && r < m_model->rowCount(); ++r) + for (int c = 0; c < data[r].size() && c < m_model->columnCount(); ++c) { + QModelIndex idx = m_model->index(r, c); + m_model->setData(idx, data[r][c], Qt::EditRole); + } + } + QAbstractItemModel *m_model; + QList m_before, m_after; + bool m_first; +}; + +class SectionMoveCommand : public QUndoCommand { +public: + SectionMoveCommand(QHeaderView *header, const QList &beforeOrder, + const QList &afterOrder, const QString &text) + : m_header(header), m_before(beforeOrder), m_after(afterOrder), m_first(true) { setText(text); } + void undo() override { restore(m_before); } + void redo() override { if (m_first) { m_first = false; return; } restore(m_after); } +private: + void restore(const QList &order) { + for (int target = 0; target < order.size(); ++target) { + int logical = order[target]; + int currentVisual = m_header->visualIndex(logical); + if (currentVisual != target) + m_header->moveSection(currentVisual, target); + } + } + QHeaderView *m_header; + QList m_before, m_after; + bool m_first; +}; + +// --------------------------------------------------------------------------- +// WrapAnywhereDelegate — already model-based (uses QModelIndex) +// --------------------------------------------------------------------------- + +void WrapAnywhereDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const { + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + QString text = opt.text; + opt.text.clear(); + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter); + + if (!text.isEmpty()) { + painter->save(); + QRect textRect = QApplication::style()->subElementRect(QStyle::SE_ItemViewItemText, &opt); + painter->setClipRect(textRect); + painter->setFont(opt.font); + QTextOption textOption; + textOption.setWrapMode(m_wrap ? QTextOption::WrapAnywhere : QTextOption::NoWrap); + textOption.setAlignment(opt.displayAlignment); + if (opt.state & QStyle::State_Selected) + painter->setPen(opt.palette.color(QPalette::HighlightedText)); + else + painter->setPen(opt.palette.color(QPalette::Text)); + painter->drawText(textRect, text, textOption); + painter->restore(); + } +} + +QSize WrapAnywhereDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const { + if (!m_wrap) return QStyledItemDelegate::sizeHint(option, index); + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + QRect textRect = QApplication::style()->subElementRect(QStyle::SE_ItemViewItemText, &opt); + int width = textRect.width(); + if (width <= 0) width = opt.rect.width(); + QTextDocument doc; + doc.setDefaultFont(opt.font); + QTextOption textOption; + textOption.setWrapMode(QTextOption::WrapAnywhere); + doc.setDefaultTextOption(textOption); + doc.setTextWidth(width); + doc.setPlainText(opt.text); + return QSize(width, qMax((int)doc.size().height(), opt.fontMetrics.height())); +} + +void WrapAnywhereDelegate::setWrapAnywhere(bool wrap) { m_wrap = wrap; } +bool WrapAnywhereDelegate::wrapAnywhere() const { return m_wrap; } + +// --------------------------------------------------------------------------- +// EditableGridWidget +// --------------------------------------------------------------------------- + +EditableGridWidget::EditableGridWidget(QTableView *view, GridMode mode, FocusManager *fm, QWidget *parent) + : QWidget(parent) + , m_fm(fm) + , m_view(view) + , m_mode(mode) + , m_isProgrammaticChange(false) + , m_lastSortColumn(-1) + , m_lastSortOrder(Qt::AscendingOrder) + , m_dragHeader(nullptr) + , m_dragLogicalIndex(-1) + , m_isDraggingSection(false) +{ + setFocusPolicy(Qt::NoFocus); + + // Take ownership + m_view->setParent(this); + + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_view); + + setupView(); + + m_undoStack = new QUndoStack(this); + fm->setUndoStack(m_undoStack); + + connect(m_undoStack, &QUndoStack::cleanChanged, this, [this](bool clean) { + emit dirtyChanged(!clean); + }); + connect(m_undoStack, &QUndoStack::indexChanged, this, [this]() { updateRowNumbers(); }); + + // MemoryDocument mode: track data changes for undo integration. + // LiveDatabase mode: the model handles its own transactions; + // intercepting dataChanged would force the entire DB into RAM. + if (m_mode == GridMode::MemoryDocument) { + if (m_view->model()) { + connect(m_view->model(), &QAbstractItemModel::dataChanged, + this, &EditableGridWidget::onDataChanged); + } + + // Stash old value when entering a cell editor + connect(fm, &FocusManager::inputWidgetEntered, this, [this](QWidget *) { + QModelIndex current = m_view->currentIndex(); + if (!current.isValid() || !m_view->model()) return; + + QAbstractItemModel *model = m_view->model(); + QModelIndex sourceIndex = current; + QAbstractItemModel *sourceModel = model; + if (auto *proxy = qobject_cast(model)) { + sourceModel = proxy->sourceModel(); + sourceIndex = proxy->mapToSource(current); + } + + QVariant existing = sourceModel->data(sourceIndex, Qt::UserRole); + if (!existing.isValid()) { + m_isProgrammaticChange = true; + sourceModel->setData(sourceIndex, + sourceModel->data(sourceIndex, Qt::EditRole), Qt::UserRole); + m_isProgrammaticChange = false; + } + }); + } + + registerShortcuts(); + setupDragToMove(); +} + +void EditableGridWidget::setupView() +{ + m_view->setFocusPolicy(Qt::ClickFocus); + m_view->setSelectionMode(QAbstractItemView::ExtendedSelection); + + m_view->horizontalHeader()->setSectionsClickable(true); + m_view->horizontalHeader()->setHighlightSections(true); + m_view->horizontalHeader()->setSectionsMovable(false); + m_view->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + m_view->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + + m_view->verticalHeader()->setSectionsClickable(true); + m_view->verticalHeader()->setHighlightSections(true); + m_view->verticalHeader()->setSectionsMovable(false); + m_view->verticalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + m_view->verticalHeader()->setSectionResizeMode(QHeaderView::Interactive); + + m_view->setSortingEnabled(false); + + m_wrapDelegate = new WrapAnywhereDelegate(m_view); + m_view->setItemDelegate(m_wrapDelegate); + + m_view->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(m_view, &QWidget::customContextMenuRequested, this, + [this](const QPoint &pos) { showRowContextMenu(pos); }); + connect(m_view->verticalHeader(), &QWidget::customContextMenuRequested, this, + [this](const QPoint &pos) { showRowContextMenu(pos); }); + connect(m_view->horizontalHeader(), &QWidget::customContextMenuRequested, this, + [this](const QPoint &pos) { showColumnContextMenu(pos); }); + connect(m_view->horizontalHeader(), &QHeaderView::sectionClicked, this, + &EditableGridWidget::onSortByColumn); + + // Install event filter on header viewports for drag-to-move + m_view->horizontalHeader()->viewport()->installEventFilter(this); + m_view->verticalHeader()->viewport()->installEventFilter(this); + // Install on view itself for in-editor key handling + m_view->installEventFilter(this); +} + +int EditableGridWidget::rowCount() const +{ + return m_view->model() ? m_view->model()->rowCount() : 0; +} + +int EditableGridWidget::colCount() const +{ + return m_view->model() ? m_view->model()->columnCount() : 0; +} + +void EditableGridWidget::registerShortcuts() +{ + // Ctrl+C → copy + m_fm->registerShortcut(QKeySequence(Qt::CTRL | Qt::Key_C), FocusManager::WhenNoInput, + [this]() { copySelection('\t'); return true; }); + + // Ctrl+V → paste + m_fm->registerShortcut(QKeySequence(Qt::CTRL | Qt::Key_V), FocusManager::WhenNoInput, + [this]() { pasteSelection(); return true; }); + + // Delete → delete selected rows + m_fm->registerShortcut(QKeySequence(Qt::Key_Delete), FocusManager::WhenNoInput, + [this]() { deleteSelectedRows(); return true; }); + + // Enter/Return → edit current cell + m_fm->registerShortcut(QKeySequence(Qt::Key_Return), FocusManager::WhenNoInput, + [this]() { + QModelIndex current = m_view->currentIndex(); + if (current.isValid()) { m_view->edit(current); return true; } + return false; + }); + m_fm->registerShortcut(QKeySequence(Qt::Key_Enter), FocusManager::WhenNoInput, + [this]() { + QModelIndex current = m_view->currentIndex(); + if (current.isValid()) { m_view->edit(current); return true; } + return false; + }); + + // Arrow keys with right-wrap + auto arrowHandler = [this](int key) -> bool { + QModelIndex current = m_view->currentIndex(); + if (!current.isValid()) return false; + int visualCol = m_view->horizontalHeader()->visualIndex(current.column()); + int visualRow = m_view->verticalHeader()->visualIndex(current.row()); + int numRows = rowCount(); + int numCols = colCount(); + if (key == Qt::Key_Up) visualRow--; + if (key == Qt::Key_Down) visualRow++; + if (key == Qt::Key_Left) { + visualCol--; + if (visualCol < 0 && visualRow > 0) { + visualCol = numCols - 1; + visualRow--; + } + } + if (key == Qt::Key_Right) { + visualCol++; + if (visualCol >= numCols && visualRow < numRows - 1) { + visualCol = 0; + visualRow++; + } + } + visualRow = qBound(0, visualRow, numRows - 1); + visualCol = qBound(0, visualCol, numCols - 1); + int r = m_view->verticalHeader()->logicalIndex(visualRow); + int c = m_view->horizontalHeader()->logicalIndex(visualCol); + m_view->setCurrentIndex(m_view->model()->index(r, c)); + return true; + }; + + m_fm->registerShortcut(QKeySequence(Qt::Key_Up), FocusManager::WhenNoInput, + [=]() { return arrowHandler(Qt::Key_Up); }); + m_fm->registerShortcut(QKeySequence(Qt::Key_Down), FocusManager::WhenNoInput, + [=]() { return arrowHandler(Qt::Key_Down); }); + m_fm->registerShortcut(QKeySequence(Qt::Key_Left), FocusManager::WhenNoInput, + [=]() { return arrowHandler(Qt::Key_Left); }); + m_fm->registerShortcut(QKeySequence(Qt::Key_Right), FocusManager::WhenNoInput, + [=]() { return arrowHandler(Qt::Key_Right); }); +} + +void EditableGridWidget::setupDragToMove() +{ + m_moveDebounceTimer = new QTimer(this); + m_moveDebounceTimer->setSingleShot(true); + m_moveDebounceTimer->setInterval(0); + + connect(m_moveDebounceTimer, &QTimer::timeout, this, [this]() { + if (!m_isDraggingSection || !m_dragHeader) return; + + int newVisual = m_dragHeader->visualIndex(m_dragLogicalIndex); + bool anyMoved = (newVisual != m_dragBeforeOrder.indexOf(m_dragLogicalIndex)); + + if (anyMoved) { + bool isHorizontal = (m_dragHeader == m_view->horizontalHeader()); + QList currentOrder; + for (int v = 0; v < m_dragHeader->count(); ++v) + currentOrder.append(m_dragHeader->logicalIndex(v)); + + QList nonSelected; + for (int li : currentOrder) + if (!m_dragSelectedSections.contains(li)) nonSelected.append(li); + + QList selectedInOrder; + for (int li : m_dragBeforeOrder) + if (m_dragSelectedSections.contains(li)) selectedInOrder.append(li); + + int insertIdx = qBound(0, newVisual, nonSelected.size()); + QList targetOrder; + for (int i = 0; i < insertIdx; ++i) targetOrder.append(nonSelected[i]); + for (int li : selectedInOrder) targetOrder.append(li); + for (int i = insertIdx; i < nonSelected.size(); ++i) targetOrder.append(nonSelected[i]); + + for (int v = 0; v < targetOrder.size(); ++v) { + int logical = targetOrder[v]; + int curVisual = m_dragHeader->visualIndex(logical); + if (curVisual != v) m_dragHeader->moveSection(curVisual, v); + } + + QList afterOrder; + for (int v = 0; v < m_dragHeader->count(); ++v) + afterOrder.append(m_dragHeader->logicalIndex(v)); + m_undoStack->push(new SectionMoveCommand(m_dragHeader, m_dragBeforeOrder, afterOrder, + isHorizontal ? "Move columns" : "Move rows")); + updateRowNumbers(); + } + + m_isDraggingSection = false; + m_dragHeader->setSectionsMovable(false); + m_dragHeader = nullptr; + }); + + auto connectMoveDebounce = [this](QHeaderView *header) { + connect(header, &QHeaderView::sectionMoved, this, [this](int, int, int) { + if (m_isDraggingSection) m_moveDebounceTimer->start(); + }); + }; + connectMoveDebounce(m_view->horizontalHeader()); + connectMoveDebounce(m_view->verticalHeader()); +} + +bool EditableGridWidget::eventFilter(QObject *obj, QEvent *event) +{ + QHeaderView *hHeader = m_view->horizontalHeader(); + QHeaderView *vHeader = m_view->verticalHeader(); + + // --- Header viewport: drag-to-move --- + if (event->type() == QEvent::MouseButtonPress) { + if (obj == hHeader->viewport() || obj == vHeader->viewport()) { + QHeaderView *header = (obj == hHeader->viewport()) ? hHeader : vHeader; + auto *me = static_cast(event); + int logicalIndex = header->logicalIndexAt(me->pos()); + if (logicalIndex >= 0 && isSectionSelected(header, logicalIndex)) { + header->setSectionsMovable(true); + m_isDraggingSection = true; + m_dragHeader = header; + m_dragLogicalIndex = logicalIndex; + m_dragBeforeOrder.clear(); + for (int v = 0; v < header->count(); ++v) + m_dragBeforeOrder.append(header->logicalIndex(v)); + + bool isHorizontal = (header == hHeader); + m_dragSelectedSections.clear(); + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + for (const QModelIndex &idx : sel) { + int li = isHorizontal ? idx.column() : idx.row(); + m_dragSelectedSections.insert(li); + } + + QItemSelection savedSel = m_view->selectionModel()->selection(); + QTimer::singleShot(0, this, [this, savedSel]() { + if (m_isDraggingSection) + m_view->selectionModel()->select(savedSel, QItemSelectionModel::ClearAndSelect); + }); + } else { + header->setSectionsMovable(false); + } + } + } + + // --- In-editor key handling --- + if (event->type() == QEvent::KeyPress && obj == m_view && m_fm->activeInput()) { + auto *ke = static_cast(event); + QAbstractItemModel *model = m_view->model(); + if (!model) return QWidget::eventFilter(obj, event); + + if (ke->key() == Qt::Key_Escape) { + QModelIndex current = m_view->currentIndex(); + if (current.isValid()) { + QVariant oldData = model->data(current, Qt::UserRole); + if (oldData.isValid()) { + m_isProgrammaticChange = true; + model->setData(current, oldData, Qt::EditRole); + model->setData(current, QVariant(), Qt::UserRole); + m_isProgrammaticChange = false; + } + } + m_view->closePersistentEditor(m_view->currentIndex()); + return true; + } + + if (ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down) { + QModelIndex current = m_view->currentIndex(); + int r = current.row(), c = current.column(); + if (ke->key() == Qt::Key_Up) r--; + if (ke->key() == Qt::Key_Down) r++; + r = qBound(0, r, rowCount() - 1); + + // Cancel current edit without saving + if (current.isValid()) { + QVariant oldData = model->data(current, Qt::UserRole); + if (oldData.isValid()) { + m_isProgrammaticChange = true; + model->setData(current, oldData, Qt::EditRole); + model->setData(current, QVariant(), Qt::UserRole); + m_isProgrammaticChange = false; + } + } + m_view->closePersistentEditor(m_view->currentIndex()); + QModelIndex target = model->index(r, c); + m_view->setCurrentIndex(target); + m_view->edit(target); + return true; + } + + if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { + QModelIndex current = m_view->currentIndex(); + int r = current.row(), c = current.column(); + + // Commit via delegate + QAbstractItemDelegate *delegate = m_view->itemDelegateForIndex(current); + QWidget *editor = m_fm->activeInput(); + if (delegate && editor) + delegate->setModelData(editor, model, current); + + // Navigate right (wrap to next row) + int visualCol = m_view->horizontalHeader()->visualIndex(c); + int visualRow = m_view->verticalHeader()->visualIndex(r); + visualCol++; + if (visualCol >= colCount()) { + visualCol = 0; + visualRow++; + } + if (visualRow < rowCount()) { + int nr = m_view->verticalHeader()->logicalIndex(visualRow); + int nc = m_view->horizontalHeader()->logicalIndex(visualCol); + QTimer::singleShot(0, this, [this, nr, nc]() { + if (m_view->model()) + m_view->setCurrentIndex(m_view->model()->index(nr, nc)); + }); + } + return true; + } + } + + return QWidget::eventFilter(obj, event); +} + +QTableView *EditableGridWidget::view() const { return m_view; } +GridMode EditableGridWidget::mode() const { return m_mode; } +QUndoStack *EditableGridWidget::undoStack() const { return m_undoStack; } +bool EditableGridWidget::isDirty() const { return !m_undoStack->isClean(); } + +void EditableGridWidget::setWordWrap(bool wrap) +{ + m_wrapDelegate->setWrapAnywhere(wrap); + m_view->setWordWrap(wrap); + if (wrap) { + m_view->resizeRowsToContents(); + } else { + m_view->verticalHeader()->setDefaultSectionSize(m_view->fontMetrics().height() + 8); + m_view->resizeRowsToContents(); + } +} + +bool EditableGridWidget::wordWrap() const { return m_wrapDelegate->wrapAnywhere(); } +void EditableGridWidget::setShowGrid(bool show) { m_view->setShowGrid(show); } + +void EditableGridWidget::setFilterRow(FilterRowWidget *filterRow) { m_filterRow = filterRow; } + +void EditableGridWidget::setExtraContextMenuCallback( + std::function callback) +{ + m_extraMenuCallback = std::move(callback); +} + +void EditableGridWidget::onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, + const QList &roles) +{ + Q_UNUSED(bottomRight); + Q_UNUSED(roles); + if (m_isProgrammaticChange || !topLeft.isValid()) return; + + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + QModelIndex sourceIndex = topLeft; + QAbstractItemModel *sourceModel = model; + if (auto *proxy = qobject_cast(model)) { + sourceModel = proxy->sourceModel(); + sourceIndex = proxy->mapToSource(topLeft); + } + + QVariant oldData = sourceModel->data(sourceIndex, Qt::UserRole); + if (!oldData.isValid()) return; + + QVariant newValue = sourceModel->data(sourceIndex, Qt::EditRole); + QString oldText = oldData.toString(); + QString newText = newValue.toString(); + + m_isProgrammaticChange = true; + sourceModel->setData(sourceIndex, QVariant(), Qt::UserRole); // clear stash + m_isProgrammaticChange = false; + + if (oldText != newText) { + m_isProgrammaticChange = true; + sourceModel->setData(sourceIndex, oldData, Qt::EditRole); // revert so undo command applies it + m_isProgrammaticChange = false; + m_undoStack->push(new EditCellCommand(sourceModel, sourceIndex, oldData, newValue)); + } +} + +void EditableGridWidget::onSortByColumn(int column) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + if (column != m_lastSortColumn) { + m_lastSortColumn = column; + m_lastSortOrder = Qt::AscendingOrder; + m_view->horizontalHeader()->setSortIndicator(-1, Qt::AscendingOrder); + return; + } + + // --- LiveDatabase mode --- + // Do not snapshot memory. Just issue the ORDER BY command via model->sort(). + // QSqlTableModel translates this to an SQL query and re-fetches lazily. + if (m_mode == GridMode::LiveDatabase) { + QAbstractItemModel *targetModel = model; + if (auto *proxy = qobject_cast(model)) { + if (proxy->sourceModel()) + targetModel = proxy->sourceModel(); + } + targetModel->sort(column, m_lastSortOrder); + m_view->horizontalHeader()->setSortIndicatorShown(true); + m_view->horizontalHeader()->setSortIndicator(column, m_lastSortOrder); + m_lastSortOrder = (m_lastSortOrder == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder; + return; + } + + // --- MemoryDocument mode --- + // Safely snapshot in-memory data for undo/redo. + int rows = rowCount(), cols = colCount(); + + QList beforeData; + for (int r = 0; r < rows; ++r) { + QVariantList row; + for (int c = 0; c < cols; ++c) + row << model->data(model->index(r, c), Qt::EditRole); + beforeData << row; + } + + model->sort(column, m_lastSortOrder); + + QList afterData; + for (int r = 0; r < rows; ++r) { + QVariantList row; + for (int c = 0; c < cols; ++c) + row << model->data(model->index(r, c), Qt::EditRole); + afterData << row; + } + + m_undoStack->push(new DataSnapshotCommand(model, beforeData, afterData, "Sort")); + m_view->horizontalHeader()->setSortIndicatorShown(true); + m_view->horizontalHeader()->setSortIndicator(column, m_lastSortOrder); + m_lastSortOrder = (m_lastSortOrder == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder; +} + +void EditableGridWidget::updateRowNumbers() +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + QHeaderView *vh = m_view->verticalHeader(); + for (int v = 0; v < rowCount(); ++v) { + int logical = vh->logicalIndex(v); + model->setHeaderData(logical, Qt::Vertical, QString::number(v + 1)); + } +} + +bool EditableGridWidget::isSectionSelected(QHeaderView *header, int logicalIndex) const +{ + QItemSelectionModel *sel = m_view->selectionModel(); + QAbstractItemModel *model = m_view->model(); + if (!sel || !model) return false; + bool isHorizontal = (header == m_view->horizontalHeader()); + if (isHorizontal) { + for (int r = 0; r < rowCount(); ++r) + if (!sel->isSelected(model->index(r, logicalIndex))) return false; + return rowCount() > 0; + } else { + for (int c = 0; c < colCount(); ++c) + if (!sel->isSelected(model->index(logicalIndex, c))) return false; + return colCount() > 0; + } +} + +// --------------------------------------------------------------------------- +// Copy / Paste / Insert / Delete — all via QAbstractItemModel +// --------------------------------------------------------------------------- + +void EditableGridWidget::copySelection(char separator) +{ + QString text = getSelectionAsText(separator); + if (!text.isEmpty()) + QApplication::clipboard()->setText(text); +} + +QString EditableGridWidget::getSelectionAsText(char separator) +{ + QAbstractItemModel *model = m_view->model(); + if (!model || !m_view->selectionModel()) return {}; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (sel.isEmpty()) return {}; + + int minVRow = rowCount(), maxVRow = -1; + int minVCol = colCount(), maxVCol = -1; + for (const auto &index : sel) { + int vr = m_view->verticalHeader()->visualIndex(index.row()); + int vc = m_view->horizontalHeader()->visualIndex(index.column()); + if (vr < minVRow) minVRow = vr; if (vr > maxVRow) maxVRow = vr; + if (vc < minVCol) minVCol = vc; if (vc > maxVCol) maxVCol = vc; + } + + QString outText; + for (int vr = minVRow; vr <= maxVRow; ++vr) { + int r = m_view->verticalHeader()->logicalIndex(vr); + QStringList rowItems; + for (int vc = minVCol; vc <= maxVCol; ++vc) { + int c = m_view->horizontalHeader()->logicalIndex(vc); + QModelIndex idx = model->index(r, c); + QString cellText; + if (m_view->selectionModel()->isSelected(idx)) + cellText = model->data(idx, Qt::DisplayRole).toString(); + rowItems << cellText; + } + outText += rowItems.join(separator) + "\n"; + } + return outText; +} + +void EditableGridWidget::pasteSelection() +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + if (m_mode == GridMode::LiveDatabase) { + // LiveDatabase: insert at end without undo wrapping + pasteSelectionAt(rowCount()); + return; + } + + // MemoryDocument: insert at selection with undo macro for reorder + int targetVisualRow = rowCount(); + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (!sel.isEmpty()) { + int minVRow = rowCount(); + for (const auto &index : sel) { + int vr = m_view->verticalHeader()->visualIndex(index.row()); + if (vr < minVRow) minVRow = vr; + } + targetVisualRow = minVRow; + } + + int endRow = rowCount(); + bool needsMove = (targetVisualRow < endRow); + + QList beforeOrder; + QHeaderView *vh = m_view->verticalHeader(); + if (needsMove) { + for (int v = 0; v < vh->count(); ++v) + beforeOrder.append(vh->logicalIndex(v)); + } + + if (needsMove) m_undoStack->beginMacro("Paste rows"); + pasteSelectionAt(endRow); + + int rowsInserted = rowCount() - endRow; + if (rowsInserted > 0 && needsMove) { + QList midOrder; + for (int v = 0; v < vh->count(); ++v) midOrder.append(vh->logicalIndex(v)); + + for (int i = 0; i < rowsInserted; ++i) { + int logicalRow = endRow + i; + int curVisual = vh->visualIndex(logicalRow); + vh->moveSection(curVisual, targetVisualRow + i); + } + + QList afterOrder; + for (int v = 0; v < vh->count(); ++v) afterOrder.append(vh->logicalIndex(v)); + m_undoStack->push(new SectionMoveCommand(vh, midOrder, afterOrder, "Move pasted rows")); + updateRowNumbers(); + } + if (needsMove) m_undoStack->endMacro(); +} + +void EditableGridWidget::pasteSelectionAt(int atRow) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + int targetCols = colCount(); + if (targetCols <= 0) return; + + QString text = QApplication::clipboard()->text(); + if (text.isEmpty()) return; + + QStringList lines = text.split(QRegularExpression("\r?\n")); + if (!lines.isEmpty() && lines.last().isEmpty()) lines.removeLast(); + if (lines.isEmpty()) return; + + // Detect separator: try tab first, then comma + char sep = '\t'; + QStringList testList = lines.first().split(QLatin1Char('\t')); + if (testList.size() != targetCols) { + QStringList commaTest = lines.first().split(QLatin1Char(',')); + if (commaTest.size() == targetCols) sep = ','; + else if (testList.size() != targetCols) return; + } + + int rowsToInsert = lines.size(); + + QAbstractItemModel *targetModel = model; + int targetRow = atRow; + if (auto *proxy = qobject_cast(model)) { + targetModel = proxy->sourceModel(); + if (atRow < proxy->rowCount()) { + QModelIndex proxyIdx = proxy->index(atRow, 0); + targetRow = proxy->mapToSource(proxyIdx).row(); + } else { + targetRow = targetModel->rowCount(); + } + } + + if (m_mode == GridMode::LiveDatabase) { + // Direct model insertion, no undo command wrapper + targetModel->insertRows(targetRow, rowsToInsert); + } else { + m_isProgrammaticChange = true; + m_undoStack->push(new RowColCommand(targetModel, targetRow, rowsToInsert, true, true)); + m_isProgrammaticChange = false; + } + + for (int i = 0; i < rowsToInsert; ++i) { + QStringList list = lines.at(i).split(QLatin1Char(sep)); + for (int vc = 0; vc < targetCols; ++vc) { + int c = m_view->horizontalHeader()->logicalIndex(vc); + QString cellText = vc < list.size() ? list.at(vc).trimmed() : ""; + m_isProgrammaticChange = true; + targetModel->setData(targetModel->index(targetRow + i, c), cellText, Qt::EditRole); + m_isProgrammaticChange = false; + } + } +} + +void EditableGridWidget::insertRows(int count, int atRow) +{ + QAbstractItemModel *model = m_view->model(); + if (!model || colCount() <= 0 || count <= 0) return; + + QAbstractItemModel *targetModel = model; + int targetRow = atRow; + if (auto *proxy = qobject_cast(model)) { + targetModel = proxy->sourceModel(); + if (atRow < proxy->rowCount()) { + QModelIndex proxyIdx = proxy->index(atRow, 0); + targetRow = proxy->mapToSource(proxyIdx).row(); + } else { + targetRow = targetModel->rowCount(); + } + } + + if (m_mode == GridMode::LiveDatabase) { + targetModel->insertRows(targetRow, count); + return; + } + m_undoStack->push(new RowColCommand(targetModel, targetRow, count, true, true)); +} + +void EditableGridWidget::deleteSelectedRows() +{ + QAbstractItemModel *model = m_view->model(); + if (!model || !m_view->selectionModel()) return; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (sel.isEmpty()) return; + + QAbstractItemModel *targetModel = model; + QSortFilterProxyModel *proxy = qobject_cast(model); + if (proxy) { + targetModel = proxy->sourceModel(); + } + + QSet rowsSet; + for (const auto &index : sel) { + if (proxy) { + rowsSet.insert(proxy->mapToSource(index).row()); + } else { + rowsSet.insert(index.row()); + } + } + QList rowsToDelete = rowsSet.values(); + std::sort(rowsToDelete.begin(), rowsToDelete.end(), std::greater()); + + if (m_mode == GridMode::LiveDatabase) { + for (int r : rowsToDelete) + targetModel->removeRow(r); + return; + } + + m_undoStack->beginMacro("Delete rows"); + for (int r : rowsToDelete) + m_undoStack->push(new RowColCommand(targetModel, r, 1, true, false)); + m_undoStack->endMacro(); +} + +void EditableGridWidget::copyColumnSelection(char separator) +{ + QAbstractItemModel *model = m_view->model(); + if (!model || !m_view->selectionModel()) return; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (sel.isEmpty()) return; + + int minCol = colCount(), maxCol = -1; + for (const auto &index : sel) { + if (index.column() < minCol) minCol = index.column(); + if (index.column() > maxCol) maxCol = index.column(); + } + + QString outText; + for (int r = 0; r < rowCount(); ++r) { + QStringList rowItems; + for (int c = minCol; c <= maxCol; ++c) + rowItems << model->data(model->index(r, c), Qt::DisplayRole).toString(); + outText += rowItems.join(separator) + "\n"; + } + QApplication::clipboard()->setText(outText); +} + +void EditableGridWidget::pasteColumnSelectionAt(int atCol) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + QAbstractItemModel *targetModel = model; + if (auto *proxy = qobject_cast(model)) + targetModel = proxy->sourceModel(); + + QString text = QApplication::clipboard()->text(); + if (text.isEmpty()) return; + + QStringList lines = text.split(QRegularExpression("\r?\n")); + if (!lines.isEmpty() && lines.last().isEmpty()) lines.removeLast(); + if (lines.isEmpty()) return; + + if (lines.size() != rowCount()) { + QMessageBox::warning(this, "Paste Error", + QString("Clipboard contains %1 rows, but table has %2.") + .arg(lines.size()).arg(rowCount())); + return; + } + + char sep = '\t'; + int colsToInsert = lines.first().split(QLatin1Char('\t')).size(); + if (colsToInsert <= 1) { + sep = ','; + colsToInsert = lines.first().split(QLatin1Char(',')).size(); + } + + if (m_mode == GridMode::LiveDatabase) { + // Direct model insertion, no undo command wrapper + targetModel->insertColumns(atCol, colsToInsert); + } else { + m_isProgrammaticChange = true; + m_undoStack->push(new RowColCommand(targetModel, atCol, colsToInsert, false, true)); + m_isProgrammaticChange = false; + } + + QSortFilterProxyModel *proxy = qobject_cast(model); + for (int r = 0; r < rowCount(); ++r) { + int targetRow = r; + if (proxy) { + QModelIndex proxyIdx = proxy->index(r, 0); + targetRow = proxy->mapToSource(proxyIdx).row(); + } + QStringList list = lines.at(r).split(QLatin1Char(sep)); + for (int c = 0; c < colsToInsert; ++c) { + QString cellText = c < list.size() ? list.at(c).trimmed() : ""; + m_isProgrammaticChange = true; + targetModel->setData(targetModel->index(targetRow, atCol + c), cellText, Qt::EditRole); + m_isProgrammaticChange = false; + } + } +} + +void EditableGridWidget::insertColumns(int count, int atCol) +{ + QAbstractItemModel *model = m_view->model(); + if (!model || rowCount() <= 0 || count <= 0) return; + + QAbstractItemModel *targetModel = model; + if (auto *proxy = qobject_cast(model)) + targetModel = proxy->sourceModel(); + + if (m_mode == GridMode::LiveDatabase) { + targetModel->insertColumns(atCol, count); + return; + } + m_undoStack->push(new RowColCommand(targetModel, atCol, count, false, true)); +} + +void EditableGridWidget::deleteSelectedColumns() +{ + QAbstractItemModel *model = m_view->model(); + if (!model || !m_view->selectionModel()) return; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (sel.isEmpty()) return; + + QAbstractItemModel *targetModel = model; + if (auto *proxy = qobject_cast(model)) + targetModel = proxy->sourceModel(); + + QSet colsSet; + for (const auto &index : sel) colsSet.insert(index.column()); + QList colsToDelete = colsSet.values(); + std::sort(colsToDelete.begin(), colsToDelete.end(), std::greater()); + + if (m_mode == GridMode::LiveDatabase) { + for (int c : colsToDelete) + targetModel->removeColumn(c); + return; + } + + m_undoStack->beginMacro("Delete cols"); + for (int c : colsToDelete) + m_undoStack->push(new RowColCommand(targetModel, c, 1, false, false)); + m_undoStack->endMacro(); +} + +// --------------------------------------------------------------------------- +// Context Menus +// --------------------------------------------------------------------------- + +void EditableGridWidget::showRowContextMenu(const QPoint &pos) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + QMenu menu(this); + + // --- Copy cell / Copy row --- + QModelIndex clickedIdx = m_view->indexAt(pos); + QAction *actCopyCell = nullptr; + QAction *actCopyRow = nullptr; + + if (clickedIdx.isValid()) { + actCopyCell = menu.addAction(QStringLiteral("Copy cell")); + actCopyRow = menu.addAction(QStringLiteral("Copy row")); + menu.addSeparator(); + } + + // --- Selection-based actions --- + QAction *actCopyTSV = nullptr, *actCopyCSV = nullptr, *actDelete = nullptr; + QAction *actInsertAbove = nullptr, *actInsertBelow = nullptr; + QAction *actPasteAbove = nullptr, *actPasteBelow = nullptr; + + int minRow = rowCount(), maxRow = -1, numRows = 0; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (!sel.isEmpty()) { + QSet rows; + for (const auto &index : sel) { + rows.insert(index.row()); + if (index.row() < minRow) minRow = index.row(); + if (index.row() > maxRow) maxRow = index.row(); + } + numRows = rows.size(); + actCopyTSV = menu.addAction("Copy Selection as TSV"); + actCopyCSV = menu.addAction("Copy Selection as CSV"); + menu.addSeparator(); + actDelete = menu.addAction("Delete Selected Rows"); + } else { + int clickedRow = m_view->rowAt(pos.y()); + if (clickedRow >= 0) { minRow = maxRow = clickedRow; numRows = 1; } + } + + if (numRows > 0) { + menu.addSeparator(); + QString rowStr = (numRows == 1) ? "1 row" : QString("%1 rows").arg(numRows); + actInsertAbove = menu.addAction(QString("Insert %1 above").arg(rowStr)); + actInsertBelow = menu.addAction(QString("Insert %1 below").arg(rowStr)); + if (!QApplication::clipboard()->text().isEmpty()) { + menu.addSeparator(); + actPasteAbove = menu.addAction("Insert from Clipboard above"); + actPasteBelow = menu.addAction("Insert from Clipboard below"); + } + } + + // --- Extra callback (KV binary ops, etc.) --- + if (m_extraMenuCallback && clickedIdx.isValid()) { + menu.addSeparator(); + m_extraMenuCallback(&menu, clickedIdx); + } + + // --- Filters and Dark theme toggles --- + menu.addSeparator(); + + QAction *actFilters = nullptr; + if (m_filterRow) { + actFilters = menu.addAction(QStringLiteral("Filters")); + actFilters->setCheckable(true); + actFilters->setChecked(m_filterRow->isFilterVisible()); + } + + QAction *res = menu.exec(m_view->viewport()->mapToGlobal(pos)); + QTimer::singleShot(0, this, [this]() { m_fm->restoreViewFocus(); }); + if (!res) return; + + // Handle results + if (res == actCopyCell && clickedIdx.isValid()) { + QString text = model->data(clickedIdx, Qt::DisplayRole).toString(); + QApplication::clipboard()->setText(text); + } else if (res == actCopyRow && clickedIdx.isValid()) { + int row = clickedIdx.row(); + QStringList cells; + for (int c = 0; c < model->columnCount(); ++c) + cells << model->data(model->index(row, c), Qt::DisplayRole).toString(); + QApplication::clipboard()->setText(cells.join(QChar('\t'))); + } else if (res == actCopyTSV) copySelection('\t'); + else if (res == actCopyCSV) copySelection(','); + else if (res == actDelete) deleteSelectedRows(); + else if (res == actInsertAbove) insertRows(numRows, minRow); + else if (res == actInsertBelow) insertRows(numRows, maxRow + 1); + else if (res == actPasteAbove) pasteSelectionAt(minRow); + else if (res == actPasteBelow) pasteSelectionAt(maxRow + 1); + else if (res == actFilters && m_filterRow) { + m_filterRow->setFilterVisible(!m_filterRow->isFilterVisible()); + } +} + +void EditableGridWidget::showColumnContextMenu(const QPoint &pos) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + QMenu menu(this); + QAction *actCopy = nullptr, *actDelete = nullptr; + QAction *actInsertLeft = nullptr, *actInsertRight = nullptr; + QAction *actPasteLeft = nullptr, *actPasteRight = nullptr; + + int minCol = colCount(), maxCol = -1, numCols = 0; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (!sel.isEmpty()) { + QSet cols; + for (const auto &index : sel) { + cols.insert(index.column()); + if (index.column() < minCol) minCol = index.column(); + if (index.column() > maxCol) maxCol = index.column(); + } + numCols = cols.size(); + actCopy = menu.addAction("Copy Columns"); + menu.addSeparator(); + actDelete = menu.addAction("Delete Selected Columns"); + } else { + int clickedCol = m_view->columnAt(pos.x()); + if (clickedCol >= 0) { minCol = maxCol = clickedCol; numCols = 1; } + } + + if (numCols > 0) { + menu.addSeparator(); + QString colStr = (numCols == 1) ? "1 col" : QString("%1 cols").arg(numCols); + actInsertLeft = menu.addAction(QString("Insert %1 left").arg(colStr)); + actInsertRight = menu.addAction(QString("Insert %1 right").arg(colStr)); + if (!QApplication::clipboard()->text().isEmpty()) { + menu.addSeparator(); + actPasteLeft = menu.addAction("Insert from Clipboard left"); + actPasteRight = menu.addAction("Insert from Clipboard right"); + } + } + + QAction *res = menu.exec(m_view->horizontalHeader()->viewport()->mapToGlobal(pos)); + QTimer::singleShot(0, this, [this]() { m_fm->restoreViewFocus(); }); + if (!res) return; + + if (res == actCopy) copyColumnSelection('\t'); + else if (res == actDelete) deleteSelectedColumns(); + else if (res == actInsertLeft) insertColumns(numCols, minCol); + else if (res == actInsertRight) insertColumns(numCols, maxCol + 1); + else if (res == actPasteLeft) pasteColumnSelectionAt(minCol); + else if (res == actPasteRight) pasteColumnSelectionAt(maxCol + 1); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/EncodingUtils.cpp b/wlx/wlxbase_wlqt/src/EncodingUtils.cpp new file mode 100644 index 0000000..9523459 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/EncodingUtils.cpp @@ -0,0 +1,122 @@ +#include + +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +QString EncodingUtils::detectEncoding(const QByteArray &data, const QString &language) +{ + if (data.isEmpty()) + return {}; + + QString lang = language; + if (lang.isEmpty()) { + // Derive from current locale without changing it + const char *loc = setlocale(LC_ALL, nullptr); + if (loc && strlen(loc) >= 2) + lang = QString::fromLatin1(loc, 2); + else + lang = "__"; + } + + EncaAnalyser analyser = enca_analyser_alloc(lang.toStdString().c_str()); + if (!analyser) + return {}; + + enca_set_threshold(analyser, 1.38); + enca_set_multibyte(analyser, 1); + enca_set_ambiguity(analyser, 1); + enca_set_garbage_test(analyser, 1); + enca_set_filtering(analyser, 0); + + EncaEncoding encoding = enca_analyse(analyser, + (unsigned char *)data.data(), (size_t)data.size()); + + QString result; + if (encoding.charset > 0 && encoding.charset != 27) { + const char *name = enca_charset_name(encoding.charset, ENCA_NAME_STYLE_ICONV); + if (name) + result = QString::fromLatin1(name); + } + + enca_analyser_free(analyser); + return result; +} + +QString EncodingUtils::detectFileEncoding(const QString &filePath, const QString &language, + int sampleSize, bool readAll) +{ + QFile file(filePath); + if (!file.open(QFile::ReadOnly)) + return {}; + + QByteArray data = readAll ? file.readAll() : file.read(sampleSize); + file.close(); + + return detectEncoding(data, language); +} + +QByteArray EncodingUtils::toUtf8(const QByteArray &data, const QString &fromEncoding) +{ + if (fromEncoding.isEmpty() || fromEncoding.compare("UTF-8", Qt::CaseInsensitive) == 0) + return data; + + gsize len; + gchar *converted = g_convert_with_fallback( + data.data(), data.size(), + "UTF-8", fromEncoding.toStdString().c_str(), + NULL, NULL, &len, NULL); + + if (converted) { + QByteArray result(converted, len); + g_free(converted); + return result; + } + return data; +} + +QByteArray EncodingUtils::fromUtf8(const QString &text, const QString &toEncoding) +{ + if (toEncoding.isEmpty() || toEncoding.compare("UTF-8", Qt::CaseInsensitive) == 0) + return text.toUtf8(); + + QByteArray utf8 = text.toUtf8(); + gsize len; + gchar *converted = g_convert_with_fallback( + utf8.data(), utf8.size(), + toEncoding.toStdString().c_str(), "UTF-8", + NULL, NULL, &len, NULL); + + if (converted) { + QByteArray result(converted, len); + g_free(converted); + return result; + } + return utf8; +} + +QString EncodingUtils::decodeToString(const QByteArray &data, const QString &language) +{ + QString encoding = detectEncoding(data, language); + if (encoding.isEmpty()) + return QString::fromUtf8(data); + + QByteArray utf8 = toUtf8(data, encoding); + return QString::fromUtf8(utf8); +} + +bool EncodingUtils::isEncaAvailable() +{ + EncaAnalyser analyser = enca_analyser_alloc("__"); + if (analyser) { + enca_analyser_free(analyser); + return true; + } + return false; +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/FilterRowWidget.cpp b/wlx/wlxbase_wlqt/src/FilterRowWidget.cpp new file mode 100644 index 0000000..0e61cdc --- /dev/null +++ b/wlx/wlxbase_wlqt/src/FilterRowWidget.cpp @@ -0,0 +1,102 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +FilterRowWidget::FilterRowWidget(QTableView *view, QWidget *parent) + : QWidget(parent) + , m_view(view) + , m_layout(new QHBoxLayout(this)) +{ + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->setSpacing(0); + setFixedHeight(24); + + // Track header geometry changes to sync widths + if (m_view->horizontalHeader()) + m_view->horizontalHeader()->installEventFilter(this); + + rebuildInputs(); +} + +void FilterRowWidget::rebuildInputs() +{ + // Clear existing + for (auto *input : m_inputs) + delete input; + m_inputs.clear(); + + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + int cols = model->columnCount(); + for (int c = 0; c < cols; ++c) { + auto *input = new QLineEdit(this); + input->setPlaceholderText(QStringLiteral("Filter...")); + input->setMaximumHeight(22); + input->setFrame(true); + input->setClearButtonEnabled(true); + + int col = c; + connect(input, &QLineEdit::textChanged, this, [this, col](const QString &text) { + emit filterChanged(col, text); + }); + + m_layout->addWidget(input); + m_inputs.append(input); + } + + syncWidths(); +} + +void FilterRowWidget::syncWidths() +{ + QHeaderView *header = m_view->horizontalHeader(); + if (!header) return; + + for (int c = 0; c < m_inputs.size(); ++c) { + int w = header->sectionSize(c); + m_inputs[c]->setFixedWidth(w); + } +} + +void FilterRowWidget::setFilterVisible(bool visible) +{ + m_visible = visible; + setVisible(visible); +} + +bool FilterRowWidget::isFilterVisible() const +{ + return m_visible; +} + +void FilterRowWidget::clearFilters() +{ + for (auto *input : m_inputs) + input->clear(); +} + +void FilterRowWidget::syncToModel() +{ + rebuildInputs(); +} + +bool FilterRowWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == m_view->horizontalHeader()) { + if (event->type() == QEvent::Resize + || event->type() == QEvent::LayoutRequest) { + syncWidths(); + } + } + return QWidget::eventFilter(obj, event); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp b/wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp new file mode 100644 index 0000000..f41e7c7 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp @@ -0,0 +1,173 @@ +#include + +#include +#include +#include +#include + +namespace QtWlPlugin { + +FilterableHeaderView::FilterableHeaderView(Qt::Orientation orientation, QWidget *parent) + : QHeaderView(orientation, parent) +{ + // Re-position filter inputs whenever sections change + connect(this, &QHeaderView::sectionResized, this, &FilterableHeaderView::adjustInputPositions); + connect(this, &QHeaderView::sectionMoved, this, &FilterableHeaderView::adjustInputPositions); + connect(this, &QHeaderView::geometriesChanged, this, &FilterableHeaderView::adjustInputPositions); +} + +void FilterableHeaderView::setFilterEnabled(bool enabled) +{ + if (m_filterEnabled == enabled) + return; + m_filterEnabled = enabled; + + if (m_filterEnabled) { + rebuildInputs(); + } else { + for (auto *input : m_inputs) + delete input; + m_inputs.clear(); + } + + // Force header to recalculate its size + updateGeometries(); + viewport()->update(); +} + +bool FilterableHeaderView::isFilterEnabled() const +{ + return m_filterEnabled; +} + +void FilterableHeaderView::clearFilters() +{ + for (auto *input : m_inputs) + input->clear(); +} + +QString FilterableHeaderView::filterText(int column) const +{ + if (column >= 0 && column < m_inputs.size()) + return m_inputs[column]->text(); + return {}; +} + +void FilterableHeaderView::setHeaderVisible(bool visible) +{ + if (m_headerVisible == visible) + return; + m_headerVisible = visible; + updateGeometries(); + viewport()->update(); +} + +bool FilterableHeaderView::isHeaderVisible() const +{ + return m_headerVisible; +} + +QSize FilterableHeaderView::sizeHint() const +{ + int h = 0; + if (m_headerVisible) { + h += QHeaderView::sizeHint().height(); + } + if (m_filterEnabled) { + h += m_filterRowHeight; + } + return QSize(QHeaderView::sizeHint().width(), h); +} + +void FilterableHeaderView::updateGeometries() +{ + int bottomMargin = 0; + if (m_filterEnabled && m_headerVisible) { + bottomMargin = m_filterRowHeight; + } + setViewportMargins(0, 0, 0, bottomMargin); + + QHeaderView::updateGeometries(); + + if (m_filterEnabled) + adjustInputPositions(); +} + +void FilterableHeaderView::paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const +{ + if (!m_headerVisible) { + painter->save(); + QStyleOptionHeader opt; + initStyleOption(&opt); + opt.section = logicalIndex; + opt.rect = rect; + opt.text.clear(); + opt.icon = QIcon(); + style()->drawControl(QStyle::CE_HeaderSection, &opt, painter, this); + painter->restore(); + return; + } + QHeaderView::paintSection(painter, rect, logicalIndex); +} + +void FilterableHeaderView::adjustInputPositions() +{ + if (!m_filterEnabled || !model()) + return; + + int cols = model()->columnCount(); + + // Rebuild if column count changed + if (m_inputs.size() != cols) + rebuildInputs(); + + int labelHeight = m_headerVisible ? QHeaderView::sizeHint().height() : 0; + + for (int i = 0; i < m_inputs.size() && i < cols; ++i) { + int logicalIdx = logicalIndex(i); + if (logicalIdx < 0 || logicalIdx >= m_inputs.size()) + continue; + + int xPos = sectionViewportPosition(logicalIdx); + int w = sectionSize(logicalIdx); + + m_inputs[logicalIdx]->setGeometry(xPos, labelHeight, w, m_filterRowHeight); + m_inputs[logicalIdx]->setVisible(!isSectionHidden(logicalIdx)); + } +} + +void FilterableHeaderView::rebuildInputs() +{ + for (auto *input : m_inputs) + delete input; + m_inputs.clear(); + + if (!model() || !m_filterEnabled) + return; + + int cols = model()->columnCount(); + m_inputs.reserve(cols); + + for (int c = 0; c < cols; ++c) { + auto *input = new QLineEdit(this); + input->setPlaceholderText(QStringLiteral("Filter...")); + input->setFrame(true); + input->setClearButtonEnabled(true); + + // Use a small font to fit the compact row height + QFont f = input->font(); + f.setPointSize(f.pointSize() - 1); + input->setFont(f); + + int col = c; + connect(input, &QLineEdit::textChanged, this, [this, col](const QString &text) { + emit filterChanged(col, text); + }); + + m_inputs.append(input); + } + + adjustInputPositions(); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/FindReplacePanel.cpp b/wlx/wlxbase_wlqt/src/FindReplacePanel.cpp new file mode 100644 index 0000000..409b71c --- /dev/null +++ b/wlx/wlxbase_wlqt/src/FindReplacePanel.cpp @@ -0,0 +1,151 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +FindReplacePanel::FindReplacePanel(FocusManager *fm, QWidget *parent) + : QWidget(parent) + , m_fm(fm) +{ + setObjectName("FindReplacePanel"); + setVisible(false); + setStyleSheet( + "QWidget#FindReplacePanel { background-color: palette(window); border-top: 1px solid palette(mid); }" + "QPushButton { border: 1px solid palette(mid); border-radius: 3px; padding: 2px 8px; background-color: palette(button); }" + "QPushButton:hover { background-color: palette(light); }" + "QPushButton:pressed { background-color: palette(midlight); }" + "QPushButton#CloseButton { border: none; background: transparent; }" + "QPushButton#CloseButton:hover { background-color: palette(light); }" + ); + + auto *panelLayout = new QVBoxLayout(this); + panelLayout->setContentsMargins(6, 6, 6, 6); + panelLayout->setSpacing(6); + + // --- Row 1: Find + Replace inputs --- + auto *row1 = new QHBoxLayout(); + row1->setSpacing(6); + + auto *lblFind = new QLabel("Find:", this); + m_txtFind = new QLineEdit(this); + m_txtFind->setPlaceholderText("Search query..."); + + m_lblReplace = new QLabel("Replace:", this); + m_txtReplace = new QLineEdit(this); + m_txtReplace->setPlaceholderText("Replacement text..."); + + row1->addWidget(lblFind); + row1->addWidget(m_txtFind, 1); + row1->addWidget(m_lblReplace); + row1->addWidget(m_txtReplace, 1); + + // --- Row 2: Options + actions --- + m_optionsRow = new QHBoxLayout(); + m_optionsRow->setSpacing(6); + + m_chkMatchCase = new QCheckBox("Match Case", this); + m_chkMatchEntire = new QCheckBox("Match Entire Cell", this); + m_chkRegex = new QCheckBox("Regular Expression", this); + + m_chkMatchCase->setFocusPolicy(Qt::NoFocus); + m_chkMatchEntire->setFocusPolicy(Qt::NoFocus); + m_chkRegex->setFocusPolicy(Qt::NoFocus); + + auto *btnFindPrev = new QPushButton("Find Previous", this); + auto *btnFindNext = new QPushButton("Find Next", this); + m_btnReplace = new QPushButton("Replace", this); + m_btnReplaceAll = new QPushButton("Replace All", this); + + btnFindPrev->setFocusPolicy(Qt::NoFocus); + btnFindNext->setFocusPolicy(Qt::NoFocus); + m_btnReplace->setFocusPolicy(Qt::NoFocus); + m_btnReplaceAll->setFocusPolicy(Qt::NoFocus); + + m_lblStatus = new QLabel(this); + m_lblStatus->setStyleSheet("color: palette(link); font-weight: bold;"); + + auto *btnClose = new QPushButton("\u2715", this); + btnClose->setObjectName("CloseButton"); + btnClose->setFixedWidth(30); + btnClose->setFlat(true); + btnClose->setFocusPolicy(Qt::NoFocus); + + m_optionsRow->addWidget(m_chkMatchCase); + m_optionsRow->addWidget(m_chkMatchEntire); + m_optionsRow->addWidget(m_chkRegex); + m_optionsRow->addWidget(btnFindPrev); + m_optionsRow->addWidget(btnFindNext); + m_optionsRow->addWidget(m_btnReplace); + m_optionsRow->addWidget(m_btnReplaceAll); + m_optionsRow->addWidget(m_lblStatus, 1); + m_optionsRow->addWidget(btnClose); + + panelLayout->addLayout(row1); + panelLayout->addLayout(m_optionsRow); + + // --- Connections --- + connect(btnFindNext, &QPushButton::clicked, this, [this]() { emit findRequested(true); }); + connect(btnFindPrev, &QPushButton::clicked, this, [this]() { emit findRequested(false); }); + connect(m_btnReplace, &QPushButton::clicked, this, &FindReplacePanel::replaceRequested); + connect(m_btnReplaceAll, &QPushButton::clicked, this, &FindReplacePanel::replaceAllRequested); + connect(btnClose, &QPushButton::clicked, this, [this]() { showPanel(false); }); + connect(m_txtFind, &QLineEdit::returnPressed, this, [this]() { emit findRequested(true); }); + connect(m_txtReplace, &QLineEdit::returnPressed, this, &FindReplacePanel::replaceRequested); + + // Register find/replace inputs as input widgets with FocusManager + fm->addInputWidget(m_txtFind); + fm->addInputWidget(m_txtReplace); + + // Register shortcuts (Always context — should work even when editing) + fm->registerShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), FocusManager::Always, + [this]() { showPanel(!isPanelVisible()); return true; }); + fm->registerShortcut(QKeySequence(Qt::CTRL | Qt::Key_R), FocusManager::Always, + [this]() { showPanel(!isPanelVisible()); return true; }); +} + +void FindReplacePanel::setReplaceEnabled(bool enabled) +{ + m_lblReplace->setVisible(enabled); + m_txtReplace->setVisible(enabled); + m_btnReplace->setVisible(enabled); + m_btnReplaceAll->setVisible(enabled); +} + +QString FindReplacePanel::findText() const { return m_txtFind->text(); } +QString FindReplacePanel::replaceText() const { return m_txtReplace->text(); } +bool FindReplacePanel::matchCase() const { return m_chkMatchCase->isChecked(); } +bool FindReplacePanel::matchEntireCell() const { return m_chkMatchEntire->isChecked(); } +bool FindReplacePanel::useRegex() const { return m_chkRegex->isChecked(); } + +void FindReplacePanel::setStatusText(const QString &text) { m_lblStatus->setText(text); } + +void FindReplacePanel::showPanel(bool show) +{ + setVisible(show); + if (show) { + m_fm->setFocusProxy(m_txtFind); + m_txtFind->setFocus(Qt::OtherFocusReason); + m_txtFind->selectAll(); + m_lblStatus->clear(); + } else { + m_fm->resetFocusProxy(); + m_lblStatus->clear(); + m_fm->restoreViewFocus(); + emit panelClosed(); + } +} + +bool FindReplacePanel::isPanelVisible() const { return isVisible(); } + +FocusManager *FindReplacePanel::focusManager() const { return m_fm; } +QLabel *FindReplacePanel::statusLabel() const { return m_lblStatus; } +QHBoxLayout *FindReplacePanel::optionsRow() const { return m_optionsRow; } + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/FocusManager.cpp b/wlx/wlxbase_wlqt/src/FocusManager.cpp new file mode 100644 index 0000000..3087136 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/FocusManager.cpp @@ -0,0 +1,332 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +bool FocusManager::s_reloadFocusTarget = false; + +FocusManager::FocusManager(QWidget *pluginRoot, QWidget *primaryView, QObject *parent) + : QObject(parent) + , m_pluginRoot(pluginRoot) + , m_primaryView(primaryView) + , m_isActive(false) + , m_undoStack(nullptr) + , m_nextShortcutId(1) +{ + installFocusGuard(); + + // Detect focus entering/leaving the plugin hierarchy + connect(qApp, &QApplication::focusChanged, this, [this](QWidget *old, QWidget *now) { + // Skip focus management when a modal dialog (save dialog, message box) + // is active — calling setActive(false) during a modal causes crashes. + if (QApplication::activeModalWidget()) + return; + + bool oldInside = old && m_pluginRoot && (old == m_pluginRoot || m_pluginRoot->isAncestorOf(old)); + bool nowInside = now && m_pluginRoot && (now == m_pluginRoot || m_pluginRoot->isAncestorOf(now)); + + if (m_isActive) { + if (oldInside && !nowInside) { + setActive(false); + } + } else { + if (nowInside && !oldInside) { + // Focus entering while inactive — bounce it back to the host + if (old) { + QPointer pOld(old); + QTimer::singleShot(0, this, [this, pOld]() { + if (pOld) { + QWidget *currentFocus = QApplication::focusWidget(); + if (currentFocus && (currentFocus == m_pluginRoot || + m_pluginRoot->isAncestorOf(currentFocus))) { + pOld->setFocus(Qt::OtherFocusReason); + } + } + }); + } else { + QTimer::singleShot(0, this, [this]() { + QWidget *currentFocus = QApplication::focusWidget(); + if (currentFocus && (currentFocus == m_pluginRoot || + m_pluginRoot->isAncestorOf(currentFocus))) { + restoreFocusToDC(); + } + }); + } + } + } + }); + + if (s_reloadFocusTarget) { + s_reloadFocusTarget = false; + QTimer::singleShot(50, this, [this]() { + setActive(true); + restoreViewFocus(); + }); + } +} + +FocusManager::~FocusManager() +{ + if (qApp) + qApp->removeEventFilter(this); +} + +// --- Activation --- + +bool FocusManager::isActive() const { return m_isActive; } + +void FocusManager::setActive(bool active) +{ + if (m_isActive == active) + return; + + m_isActive = active; + + if (!active) { + m_activeInput = nullptr; + m_pluginRoot->clearFocus(); + if (m_pluginRoot->parentWidget()) + m_pluginRoot->parentWidget()->setFocus(Qt::OtherFocusReason); + emit deactivated(); + } else { + emit activated(); + } +} + +// --- Input widget tracking --- + +void FocusManager::addInputWidget(QWidget *w) +{ + if (w) + m_extraInputWidgets.insert(w); +} + +void FocusManager::removeInputWidget(QWidget *w) +{ + m_extraInputWidgets.remove(w); +} + +bool FocusManager::isInputWidget(QWidget *w) const +{ + if (!w) + return false; + if (m_extraInputWidgets.contains(w)) + return true; + // A descendant of primaryView (but not primaryView itself) is an input widget + // (e.g. a cell editor spawned inside a QTableWidget) + return w != m_primaryView && m_primaryView->isAncestorOf(w); +} + +QWidget *FocusManager::activeInput() const { return m_activeInput; } + +// --- Focus proxy --- + +void FocusManager::setFocusProxy(QWidget *proxy) +{ + if (m_pluginRoot) + m_pluginRoot->setFocusProxy(proxy); +} + +void FocusManager::resetFocusProxy() +{ + if (m_pluginRoot && m_primaryView) + m_pluginRoot->setFocusProxy(m_primaryView); +} + +// --- Shortcut registration --- + +FocusManager::ShortcutId FocusManager::registerShortcut( + const QKeySequence &keys, ShortcutContext ctx, std::function handler) +{ + ShortcutId id = m_nextShortcutId++; + m_shortcuts.append({id, keys, ctx, std::move(handler)}); + return id; +} + +void FocusManager::unregisterShortcut(ShortcutId id) +{ + m_shortcuts.erase( + std::remove_if(m_shortcuts.begin(), m_shortcuts.end(), + [id](const RegisteredShortcut &s) { return s.id == id; }), + m_shortcuts.end()); +} + +// --- Optional undo/redo --- + +void FocusManager::setUndoStack(QUndoStack *stack) +{ + // Unregister previous undo shortcuts + for (ShortcutId id : m_undoShortcutIds) + unregisterShortcut(id); + m_undoShortcutIds.clear(); + + m_undoStack = stack; + + if (stack) { + // Ctrl+Z → undo + m_undoShortcutIds.append(registerShortcut( + QKeySequence(Qt::CTRL | Qt::Key_Z), WhenNoInput, + [stack]() { + if (stack->canUndo()) { stack->undo(); return true; } + return false; + })); + + // Ctrl+Shift+Z → redo + m_undoShortcutIds.append(registerShortcut( + QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Z), WhenNoInput, + [stack]() { + if (stack->canRedo()) { stack->redo(); return true; } + return false; + })); + + // Ctrl+Y → redo + m_undoShortcutIds.append(registerShortcut( + QKeySequence(Qt::CTRL | Qt::Key_Y), WhenNoInput, + [stack]() { + if (stack->canRedo()) { stack->redo(); return true; } + return false; + })); + } +} + +QUndoStack *FocusManager::undoStack() const { return m_undoStack; } + +// --- Saved focus --- + +void FocusManager::saveFocusWidget(QWidget *w) { m_savedFocusWidget = w; } + +// --- Access --- + +QWidget *FocusManager::pluginRoot() const { return m_pluginRoot; } +QWidget *FocusManager::primaryView() const { return m_primaryView; } + +// --- Focus restoration --- + +void FocusManager::restoreViewFocus() +{ + if (m_primaryView) + m_primaryView->setFocus(Qt::OtherFocusReason); +} + +void FocusManager::expectReloadFocus() +{ + s_reloadFocusTarget = true; + QTimer::singleShot(1000, []() { + s_reloadFocusTarget = false; + }); +} + +void FocusManager::restoreFocusToDC() +{ + if (m_savedFocusWidget) { + m_savedFocusWidget->setFocus(Qt::OtherFocusReason); + } else if (m_pluginRoot) { + if (QWidget *fw = QApplication::focusWidget()) { + if (fw == m_pluginRoot || fw->isAncestorOf(m_pluginRoot) || + m_pluginRoot->isAncestorOf(fw)) + fw->clearFocus(); + } + } +} + +void FocusManager::installFocusGuard() +{ + if (qApp) + qApp->installEventFilter(this); + if (m_pluginRoot && m_primaryView) + m_pluginRoot->setFocusProxy(m_primaryView); +} + +// --- Event filter (the critical focus/shortcut engine) --- + +bool FocusManager::eventFilter(QObject *obj, QEvent *event) +{ + QWidget *w = qobject_cast(obj); + + // --- Geometry-based click detection --- + if (event->type() == QEvent::MouseButtonPress) { + if (!m_pluginRoot) return false; + + auto *me = static_cast(event); + const QPoint gp = me->globalPosition().toPoint(); + const QRect gr(m_pluginRoot->mapToGlobal(QPoint(0, 0)), m_pluginRoot->size()); + + if (m_isActive && !gr.contains(gp)) { + // Click outside plugin — deactivate + setActive(false); + return false; + } else if (!m_isActive && gr.contains(gp)) { + // Click inside plugin — activate + m_isActive = true; + emit activated(); + if (w && (w->focusPolicy() & Qt::ClickFocus)) { + w->setFocus(Qt::MouseFocusReason); + } else if (m_primaryView) { + m_primaryView->setFocus(Qt::MouseFocusReason); + } + } + } + + // --- FocusIn tracking --- + if (event->type() == QEvent::FocusIn) { + if (w && m_pluginRoot && (w == m_pluginRoot || m_pluginRoot->isAncestorOf(w))) { + auto *fe = static_cast(event); + if (!m_isActive && fe->reason() == Qt::OtherFocusReason) { + // Programmatic focus entry while inactive — don't activate + return false; + } + if (!m_isActive) { + m_isActive = true; + emit activated(); + } + if (isInputWidget(w)) { + m_activeInput = w; + emit inputWidgetEntered(w); + } + } + } + + // --- KeyPress: shortcut dispatch --- + if (event->type() == QEvent::KeyPress && m_isActive) { + auto *ke = static_cast(event); + QKeySequence pressed(ke->modifiers() | ke->key()); + + for (const auto &shortcut : m_shortcuts) { + if (shortcut.keys.count() != 1) + continue; + if (pressed[0] != shortcut.keys[0]) + continue; + + // Check context + if (shortcut.ctx == WhenNoInput && m_activeInput) + continue; + + if (shortcut.handler && shortcut.handler()) + return true; + } + } + + // --- ChildAdded: NoFocus enforcement --- + if (event->type() == QEvent::ChildAdded) { + if (w && m_pluginRoot && (w == m_pluginRoot || m_pluginRoot->isAncestorOf(w))) { + auto *ce = static_cast(event); + if (auto *childWidget = qobject_cast(ce->child())) { + if (!isInputWidget(childWidget)) + childWidget->setFocusPolicy(Qt::NoFocus); + } + } + } + + return QObject::eventFilter(obj, event); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/PluginSplitView.cpp b/wlx/wlxbase_wlqt/src/PluginSplitView.cpp new file mode 100644 index 0000000..cf02f26 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/PluginSplitView.cpp @@ -0,0 +1,34 @@ +#include + +namespace QtWlPlugin { + +PluginSplitView::PluginSplitView(QWidget *leftPanel, QWidget *rightContent, + QWidget *parent) + : QSplitter(Qt::Horizontal, parent) + , m_left(leftPanel) + , m_right(rightContent) +{ + addWidget(m_left); + addWidget(m_right); + + m_left->setMinimumWidth(100); + m_left->setMaximumWidth(350); + + setStretchFactor(0, 0); // Left: fixed + setStretchFactor(1, 1); // Right: stretches + + setSizes({180, 600}); + setChildrenCollapsible(false); + + setHandleWidth(3); +} + +void PluginSplitView::setLeftWidth(int pixels) +{ + setSizes({pixels, width() - pixels}); +} + +QWidget *PluginSplitView::leftPanel() const { return m_left; } +QWidget *PluginSplitView::rightContent() const { return m_right; } + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/PluginStatusBar.cpp b/wlx/wlxbase_wlqt/src/PluginStatusBar.cpp new file mode 100644 index 0000000..d2be345 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/PluginStatusBar.cpp @@ -0,0 +1,136 @@ +#include + +#include +#include +#include + +namespace QtWlPlugin { + +PluginStatusBar::PluginStatusBar(QWidget *parent) + : QWidget(parent) + , m_layout(new QHBoxLayout(this)) +{ + m_layout->setContentsMargins(4, 2, 4, 2); + m_layout->setSpacing(0); + setFixedHeight(22); + setStyleSheet(QStringLiteral( + "PluginStatusBar { border-top: 1px solid #c0c0c0; }" + ).replace(QStringLiteral("PluginStatusBar"), + QStringLiteral("QtWlPlugin--PluginStatusBar"))); + + QFont smallFont = font(); + smallFont.setPointSize(9); + + m_encodingLabel = new QLabel(this); + m_encodingLabel->setFont(smallFont); + + m_formatLabel = new QLabel(this); + m_formatLabel->setFont(smallFont); + + m_rowLabel = new QLabel(this); + m_rowLabel->setFont(smallFont); + + rebuild(); +} + +QFrame *PluginStatusBar::createSeparator() +{ + auto *sep = new QFrame(this); + sep->setFrameShape(QFrame::VLine); + sep->setFrameShadow(QFrame::Sunken); + sep->setFixedWidth(2); + return sep; +} + +void PluginStatusBar::rebuild() +{ + // Remove all items from layout (without deleting the labels themselves) + while (m_layout->count() > 0) { + QLayoutItem *item = m_layout->takeAt(0); + // Delete separators and spacers, keep our persistent labels + if (item->widget() + && item->widget() != m_encodingLabel + && item->widget() != m_formatLabel + && item->widget() != m_rowLabel + && !m_extras.values().contains(qobject_cast(item->widget()))) { + delete item->widget(); + } + delete item; + } + + // Add extras first (sorted by key) + QStringList keys = m_extras.keys(); + keys.sort(); + for (const auto &key : keys) { + if (m_layout->count() > 0) + m_layout->addWidget(createSeparator()); + m_layout->addWidget(m_extras[key]); + } + + // Encoding + if (!m_encodingLabel->text().isEmpty()) { + if (m_layout->count() > 0) + m_layout->addWidget(createSeparator()); + m_layout->addWidget(m_encodingLabel); + } + + // Format + if (!m_formatLabel->text().isEmpty()) { + if (m_layout->count() > 0) + m_layout->addWidget(createSeparator()); + m_layout->addWidget(m_formatLabel); + } + + // Spacer → row count on right + m_layout->addStretch(1); + + if (!m_rowLabel->text().isEmpty()) { + m_layout->addWidget(createSeparator()); + m_layout->addWidget(m_rowLabel); + } +} + +void PluginStatusBar::setEncoding(const QString &encoding) +{ + m_encodingLabel->setText(QStringLiteral(" %1 ").arg(encoding)); + rebuild(); +} + +void PluginStatusBar::setFormatInfo(const QString &info) +{ + m_formatLabel->setText(QStringLiteral(" %1 ").arg(info)); + rebuild(); +} + +void PluginStatusBar::setRowCount(int filtered, int total) +{ + if (filtered == total) + m_rowLabel->setText(QStringLiteral(" Rows: %1 ").arg(total)); + else + m_rowLabel->setText(QStringLiteral(" Rows: %1/%2 ").arg(filtered).arg(total)); + rebuild(); +} + +void PluginStatusBar::setExtraInfo(const QString &key, const QString &value) +{ + QLabel *label = m_extras.value(key, nullptr); + if (!label) { + QFont smallFont = font(); + smallFont.setPointSize(9); + label = new QLabel(this); + label->setFont(smallFont); + m_extras[key] = label; + } + label->setText(QStringLiteral(" %1: %2 ").arg(key, value)); + rebuild(); +} + +void PluginStatusBar::removeExtraInfo(const QString &key) +{ + if (m_extras.contains(key)) { + delete m_extras.take(key); + rebuild(); + } +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/PluginToolBar.cpp b/wlx/wlxbase_wlqt/src/PluginToolBar.cpp new file mode 100644 index 0000000..74912dd --- /dev/null +++ b/wlx/wlxbase_wlqt/src/PluginToolBar.cpp @@ -0,0 +1,65 @@ +#include +#include + +#include +#include +#include + +namespace QtWlPlugin { + +PluginToolBar::PluginToolBar(FocusManager *fm, QWidget *parent) + : QToolBar(parent) + , m_fm(fm) +{ + setFocusPolicy(Qt::NoFocus); + setStyleSheet( + "QToolBar { spacing: 2px; }" + "QToolButton { padding: 2px 4px; margin: 1px; }" + ); + enforceNoFocus(); +} + +QAction *PluginToolBar::addToolAction(const QString &text, const QKeySequence &shortcut, int ctx) +{ + QAction *action = new QAction(text, this); + addAction(action); + + if (!shortcut.isEmpty()) { + action->setToolTip(text + " (" + shortcut.toString(QKeySequence::NativeText) + ")"); + + if (m_fm) { + m_fm->registerShortcut( + shortcut, static_cast(ctx), + [action]() { action->trigger(); return true; }); + } + } else { + action->setToolTip(text); + } + + // Restore focus to primary view after action trigger + connect(action, &QAction::triggered, this, [this]() { + QTimer::singleShot(0, this, [this]() { + if (m_fm) + m_fm->restoreViewFocus(); + }); + }); + + return action; +} + +void PluginToolBar::actionEvent(QActionEvent *event) +{ + QToolBar::actionEvent(event); + enforceNoFocus(); +} + +void PluginToolBar::enforceNoFocus() +{ + for (QAction *action : actions()) { + QWidget *w = widgetForAction(action); + if (w) + w->setFocusPolicy(Qt::NoFocus); + } +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/ScopedFindReplacePanel.cpp b/wlx/wlxbase_wlqt/src/ScopedFindReplacePanel.cpp new file mode 100644 index 0000000..d611b08 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/ScopedFindReplacePanel.cpp @@ -0,0 +1,34 @@ +#include +#include + +#include +#include +#include + +namespace QtWlPlugin { + +ScopedFindReplacePanel::ScopedFindReplacePanel(FocusManager *fm, QWidget *parent) + : FindReplacePanel(fm, parent) +{ + auto *lblScope = new QLabel("Scope:", this); + m_comboScope = new QComboBox(this); + m_comboScope->setFocusPolicy(Qt::NoFocus); + + // Insert at the beginning of the options row (before checkboxes) + QHBoxLayout *row = optionsRow(); + row->insertWidget(0, lblScope); + row->insertWidget(1, m_comboScope); +} + +void ScopedFindReplacePanel::setScopes(const QStringList &scopes) +{ + m_comboScope->clear(); + m_comboScope->addItems(scopes); +} + +QString ScopedFindReplacePanel::currentScope() const +{ + return m_comboScope->currentText(); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/SequentialRowProxyModel.cpp b/wlx/wlxbase_wlqt/src/SequentialRowProxyModel.cpp new file mode 100644 index 0000000..e6aac0f --- /dev/null +++ b/wlx/wlxbase_wlqt/src/SequentialRowProxyModel.cpp @@ -0,0 +1,22 @@ +#include + +namespace QtWlPlugin { + +SequentialRowProxyModel::SequentialRowProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +SequentialRowProxyModel::~SequentialRowProxyModel() +{ +} + +QVariant SequentialRowProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical && role == Qt::DisplayRole) { + return section + 1; + } + return QSortFilterProxyModel::headerData(section, orientation, role); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/ThemeManager.cpp b/wlx/wlxbase_wlqt/src/ThemeManager.cpp new file mode 100644 index 0000000..f14e1b5 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/ThemeManager.cpp @@ -0,0 +1,260 @@ +#include + +#include +#include + +namespace QtWlPlugin { + +ThemeManager::Theme ThemeManager::s_current = ThemeManager::Dark; + +ThemeManager::Theme ThemeManager::detectSystemTheme() +{ + // Inspect the application palette's Window background. + // The plugin runs inside Double Commander's process, so the palette + // reflects DC's active Qt/GTK theme. A dark window colour means a + // dark system theme. + const QColor bg = qApp ? qApp->palette().color(QPalette::Window) + : QColor(30, 30, 30); // fallback dark + // HSP colour model: perceived brightness + // sqrt(0.299*R² + 0.587*G² + 0.114*B²) + const double brightness = std::sqrt( + 0.299 * bg.redF() * bg.redF() + + 0.587 * bg.greenF() * bg.greenF() + + 0.114 * bg.blueF() * bg.blueF()); + return (brightness < 0.5) ? Dark : Light; +} + +void ThemeManager::applyTheme(QWidget *root, Theme theme) +{ + s_current = theme; + + if (theme == Light) { + root->setStyleSheet(QString()); + } else { + root->setStyleSheet(darkStylesheet()); + } +} + +bool ThemeManager::isDark() +{ + return s_current == Dark; +} + +QString ThemeManager::darkStylesheet() +{ + return QStringLiteral(R"( + QWidget { + background-color: #1e1e1e; + color: #d4d4d4; + selection-background-color: #264f78; + selection-color: #ffffff; + } + + QTableView { + background-color: #1e1e1e; + alternate-background-color: #252526; + gridline-color: #3c3c3c; + border: 1px solid #3c3c3c; + } + + QHeaderView::section { + background-color: #333333; + color: #d4d4d4; + border: 1px solid #3c3c3c; + padding: 3px 5px; + font-weight: bold; + } + + QHeaderView::section:hover { + background-color: #404040; + } + + QTreeView { + background-color: #252526; + alternate-background-color: #2d2d2d; + border: 1px solid #3c3c3c; + } + + QTreeView::item:selected { + background-color: #264f78; + } + + QTreeView::item:hover { + background-color: #2a2d2e; + } + + QListWidget { + background-color: #252526; + border: 1px solid #3c3c3c; + } + + QListWidget::item:selected { + background-color: #264f78; + } + + QListWidget::item:hover { + background-color: #2a2d2e; + } + + QToolBar { + background-color: #333333; + border-bottom: 1px solid #3c3c3c; + spacing: 2px; + } + + QToolBar QLabel { + color: #d4d4d4; + } + + QToolButton { + background-color: transparent; + color: #d4d4d4; + border: 1px solid transparent; + padding: 3px 6px; + border-radius: 3px; + } + + QToolButton:hover { + background-color: #404040; + border-color: #505050; + } + + QToolButton:checked { + background-color: #264f78; + border-color: #3a7bd5; + } + + QLineEdit { + background-color: #3c3c3c; + color: #d4d4d4; + border: 1px solid #555555; + padding: 2px 4px; + border-radius: 2px; + } + + QLineEdit:focus { + border-color: #007acc; + } + + QComboBox { + background-color: #3c3c3c; + color: #d4d4d4; + border: 1px solid #555555; + padding: 2px 4px; + border-radius: 2px; + } + + QComboBox::drop-down { + border-left: 1px solid #555555; + } + + QComboBox QAbstractItemView { + background-color: #252526; + color: #d4d4d4; + selection-background-color: #264f78; + } + + QMenu { + background-color: #252526; + color: #d4d4d4; + border: 1px solid #3c3c3c; + } + + QMenu::item:selected { + background-color: #264f78; + } + + QMenu::separator { + height: 1px; + background: #3c3c3c; + margin: 4px 8px; + } + + QTabWidget::pane { + border: 1px solid #3c3c3c; + background-color: #1e1e1e; + } + + QTabBar::tab { + background-color: #2d2d2d; + color: #969696; + border: 1px solid #3c3c3c; + padding: 4px 12px; + margin-right: 1px; + } + + QTabBar::tab:selected { + background-color: #1e1e1e; + color: #d4d4d4; + border-bottom-color: #1e1e1e; + } + + QTabBar::tab:hover { + background-color: #353535; + } + + QPlainTextEdit { + background-color: #1e1e1e; + color: #d4d4d4; + border: 1px solid #3c3c3c; + font-family: "Cascadia Code", "Fira Code", "Source Code Pro", monospace; + } + + QSplitter::handle { + background-color: #3c3c3c; + } + + QSplitter::handle:hover { + background-color: #007acc; + } + + QScrollBar:vertical { + background-color: #1e1e1e; + width: 12px; + border: none; + } + + QScrollBar::handle:vertical { + background-color: #424242; + min-height: 20px; + border-radius: 3px; + margin: 2px; + } + + QScrollBar::handle:vertical:hover { + background-color: #555555; + } + + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0px; + } + + QScrollBar:horizontal { + background-color: #1e1e1e; + height: 12px; + border: none; + } + + QScrollBar::handle:horizontal { + background-color: #424242; + min-width: 20px; + border-radius: 3px; + margin: 2px; + } + + QScrollBar::handle:horizontal:hover { + background-color: #555555; + } + + QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { + width: 0px; + } + + /* Status bar */ + QFrame[frameShape="5"] { + color: #555555; + } + )"); +} + +} // namespace QtWlPlugin