Skip to content

Commit d9a1a9b

Browse files
committed
LMDB: add utilities that allow to explore LMDB contents
Intended for debugging and testing purposes
1 parent 4e7c0e3 commit d9a1a9b

File tree

4 files changed

+422
-0
lines changed

4 files changed

+422
-0
lines changed

CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ option(WITH_MAPSERVER "Enable (experimental) support for the mapserver library"
102102
option(WITH_RIAK "Use Riak as a cache backend" OFF)
103103
option(WITH_GDAL "Choose if GDAL raster support should be built in" ON)
104104
option(WITH_MAPCACHE_DETAIL "Build coverage analysis tool for SQLite caches" ON)
105+
option(WITH_LMDB_UTILS "Build LMDB utilities for debugging" OFF)
106+
107+
# Ensure WITH_LMDB_UTILS is only enabled if WITH_LMDB is enabled
108+
if(NOT WITH_LMDB)
109+
if(WITH_LMDB_UTILS)
110+
message(WARNING "WITH_LMDB_UTILS is ON but WITH_LMDB is OFF. Disabling WITH_LMDB_UTILS.")
111+
endif()
112+
set(WITH_LMDB_UTILS OFF CACHE BOOL "Build LMDB utilities for debugging" FORCE)
113+
endif()
105114

106115
find_package(PNG)
107116
if(PNG_FOUND)
@@ -376,6 +385,7 @@ status_optional_component("RIAK" "${USE_RIAK}" "${RIAK_LIBRARY}")
376385
status_optional_component("GDAL" "${USE_GDAL}" "${GDAL_LIBRARY}")
377386
message(STATUS " * Optional features")
378387
status_optional_feature("MAPCACHE_DETAIL" "${WITH_MAPCACHE_DETAIL}")
388+
status_optional_feature("LMDB_UTILS" "${WITH_LMDB_UTILS}")
379389

380390
INSTALL(TARGETS mapcache DESTINATION ${CMAKE_INSTALL_LIBDIR})
381391

@@ -384,6 +394,8 @@ add_subdirectory(cgi)
384394
add_subdirectory(apache)
385395
add_subdirectory(nginx)
386396

397+
add_subdirectory(contrib/lmdb_utils)
398+
387399
if (WITH_MAPCACHE_DETAIL)
388400
add_subdirectory(contrib/mapcache_detail)
389401
endif (WITH_MAPCACHE_DETAIL)

contrib/lmdb_utils/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
if(WITH_LMDB_UTILS)
2+
find_package(LMDB REQUIRED)
3+
include_directories(${LMDB_INCLUDE_DIR})
4+
add_executable(mapcache_lmdb_list ${CMAKE_CURRENT_SOURCE_DIR}/mapcache_lmdb_list.c)
5+
target_include_directories(mapcache_lmdb_list PRIVATE ${APR_INCLUDE_DIR})
6+
target_link_libraries(mapcache_lmdb_list PRIVATE ${LMDB_LIBRARY} ${APR_LIBRARY})
7+
add_executable(mapcache_lmdb_get ${CMAKE_CURRENT_SOURCE_DIR}/mapcache_lmdb_get.c)
8+
target_include_directories(mapcache_lmdb_get PRIVATE ${APR_INCLUDE_DIR})
9+
target_link_libraries(mapcache_lmdb_get PRIVATE ${LMDB_LIBRARY} ${APR_LIBRARY})
10+
11+
INSTALL(TARGETS mapcache_lmdb_list mapcache_lmdb_get RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
12+
endif(WITH_LMDB_UTILS)
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/******************************************************************************
2+
*
3+
* Project: MapCache
4+
* Purpose: Extract raw value of a single key from a LMDB database
5+
* Author: Maris Nartiss
6+
*
7+
******************************************************************************
8+
* Copyright (c) 2025 Regents of the University of Minnesota.
9+
*
10+
* Permission is hereby granted, free of charge, to any person obtaining a
11+
* copy of this software and associated documentation files (the "Software"),
12+
* to deal in the Software without restriction, including without limitation
13+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
14+
* and/or sell copies of the Software, and to permit persons to whom the
15+
* Software is furnished to do so, subject to the following conditions:
16+
*
17+
* The above copyright notice and this permission notice shall be included in
18+
* all copies of this Software or works derived from this Software.
19+
*
20+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21+
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23+
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26+
* DEALINGS IN THE SOFTWARE.
27+
****************************************************************************/
28+
29+
#include <stdio.h>
30+
#include <stdlib.h>
31+
#include <string.h>
32+
#include <apr_time.h>
33+
#include <apr_getopt.h>
34+
#include <apr_pools.h>
35+
#include <apr_date.h>
36+
#include "lmdb.h"
37+
38+
void fail(const char *msg, int rc) {
39+
fprintf(stderr, "%s: %s\n", msg, mdb_strerror(rc));
40+
exit(EXIT_FAILURE);
41+
}
42+
43+
static const apr_getopt_option_t options[] = {
44+
{ "dbpath", 'd', TRUE, "Path to the LMDB database directory" },
45+
{ "key", 'k', TRUE, "Key to retrieve" },
46+
{ "output", 'o', TRUE, "Output file (default: output.bin)" },
47+
{ "help", 'h', FALSE, "Show help" },
48+
{ NULL, 0, 0, NULL }
49+
};
50+
51+
void usage(const char *progname) {
52+
int i = 0;
53+
printf("usage: %s options\n", progname);
54+
while (options[i].name) {
55+
if (options[i].optch < 256) {
56+
if (options[i].has_arg == TRUE) {
57+
printf("-%c|--%s [value]: %s\n", options[i].optch, options[i].name, options[i].description);
58+
} else {
59+
printf("-%c|--%s: %s\n", options[i].optch, options[i].name, options[i].description);
60+
}
61+
} else {
62+
if (options[i].has_arg == TRUE) {
63+
printf(" --%s [value]: %s\n", options[i].name, options[i].description);
64+
} else {
65+
printf(" --%s: %s\n", options[i].name, options[i].description);
66+
}
67+
}
68+
i++;
69+
}
70+
exit(EXIT_FAILURE);
71+
}
72+
73+
int main(int argc, const char * const *argv) {
74+
const char *db_path = NULL;
75+
const char *key_str = NULL;
76+
const char *out_filename = "output.bin";
77+
int rc;
78+
MDB_env *env;
79+
MDB_txn *txn;
80+
MDB_dbi dbi;
81+
MDB_val key, data;
82+
apr_pool_t *pool = NULL;
83+
apr_getopt_t *opt;
84+
int optch;
85+
const char *optarg;
86+
87+
apr_initialize();
88+
apr_pool_create(&pool, NULL);
89+
apr_getopt_init(&opt, pool, argc, argv);
90+
91+
while ((rc = apr_getopt_long(opt, options, &optch, &optarg)) == APR_SUCCESS) {
92+
switch (optch) {
93+
case 'h':
94+
usage(argv[0]);
95+
break;
96+
case 'd':
97+
db_path = optarg;
98+
break;
99+
case 'k':
100+
key_str = optarg;
101+
break;
102+
case 'o':
103+
out_filename = optarg;
104+
break;
105+
}
106+
}
107+
108+
if (rc != APR_EOF) {
109+
fprintf(stderr, "Error: Invalid option.\n");
110+
usage(argv[0]);
111+
}
112+
113+
if (!db_path || !key_str) {
114+
fprintf(stderr, "Error: --dbpath and --key are required.\n");
115+
usage(argv[0]);
116+
}
117+
118+
// Create and open the environment
119+
rc = mdb_env_create(&env);
120+
if (rc) fail("mdb_env_create", rc);
121+
122+
rc = mdb_env_open(env, db_path, MDB_RDONLY, 0664);
123+
if (rc) {
124+
mdb_env_close(env);
125+
fail("mdb_env_open", rc);
126+
}
127+
128+
// Begin a read-only transaction
129+
rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn);
130+
if (rc) {
131+
mdb_env_close(env);
132+
fail("mdb_txn_begin", rc);
133+
}
134+
135+
// Open the database
136+
rc = mdb_dbi_open(txn, NULL, 0, &dbi);
137+
if (rc) {
138+
mdb_txn_abort(txn);
139+
mdb_env_close(env);
140+
fail("mdb_dbi_open", rc);
141+
}
142+
143+
// Prepare the key, which is a null-terminated string in the DB
144+
key.mv_size = strlen(key_str) + 1;
145+
key.mv_data = (void *)key_str;
146+
147+
// Retrieve the data
148+
rc = mdb_get(txn, dbi, &key, &data);
149+
150+
if (rc == MDB_SUCCESS) {
151+
apr_time_t timestamp;
152+
char time_str[APR_RFC822_DATE_LEN];
153+
FILE *fp;
154+
fp = fopen(out_filename, "wb");
155+
if (!fp) {
156+
mdb_txn_abort(txn);
157+
mdb_env_close(env);
158+
perror("Unable to open output file");
159+
return EXIT_FAILURE;
160+
} else {
161+
// Check for special blank tile encoding ('#' marker)
162+
if (data.mv_size > 1 && ((char *)data.mv_data)[0] == '#') {
163+
if (data.mv_size == 5 + sizeof(apr_time_t)) {
164+
const unsigned char *color = (unsigned char *)data.mv_data + 1;
165+
printf("Key found. Blank tile, color #%02x%02x%02x%02x. "
166+
"Writing description to \"%s\"\n",
167+
color[0], color[1], color[2], color[3], out_filename);
168+
fprintf(fp, "Blank tile, RGBA: #%02x%02x%02x%02x\n",
169+
color[0], color[1], color[2], color[3]);
170+
} else {
171+
printf("Key found. Blank tile marker found, but data "
172+
"size is unexpected (%zu bytes). Writing as is.\n",
173+
data.mv_size);
174+
fwrite(data.mv_data, 1, data.mv_size, fp);
175+
}
176+
177+
// Extract timestamp from the end of the data
178+
memcpy(&timestamp, (char *)data.mv_data + 5, sizeof(apr_time_t));
179+
180+
// Convert to human-readable format
181+
apr_rfc822_date(time_str, timestamp);
182+
183+
printf("Timestamp: %s\n", time_str);
184+
} else if (data.mv_size >= sizeof(apr_time_t)) {
185+
// Regular tile data, strip the trailing timestamp
186+
size_t image_size = data.mv_size - sizeof(apr_time_t);
187+
188+
// Extract timestamp from the end of the data
189+
memcpy(&timestamp, (char *)data.mv_data + image_size, sizeof(apr_time_t));
190+
191+
// Convert to human-readable format
192+
apr_rfc822_date(time_str, timestamp);
193+
194+
printf("Key found. Writing %zu image bytes to \"%s\"\n",
195+
image_size, out_filename);
196+
printf("Timestamp: %s\n", time_str);
197+
fwrite(data.mv_data, 1, image_size, fp);
198+
} else {
199+
// Data is smaller than a timestamp, write as is
200+
printf("Key found. Data size (%zu) is smaller than a "
201+
"timestamp, writing as is to \"%s\"\n",
202+
data.mv_size, out_filename);
203+
fwrite(data.mv_data, 1, data.mv_size, fp);
204+
}
205+
fclose(fp);
206+
}
207+
} else if (rc == MDB_NOTFOUND) {
208+
fprintf(stderr, "Key '%s' not found.\n", key_str);
209+
} else {
210+
fail("mdb_get", rc);
211+
}
212+
213+
// Clean up
214+
mdb_txn_abort(txn);
215+
mdb_env_close(env);
216+
217+
apr_pool_destroy(pool);
218+
apr_terminate();
219+
220+
return (rc == MDB_SUCCESS) ? EXIT_SUCCESS : EXIT_FAILURE;
221+
}

0 commit comments

Comments
 (0)