@ohos-ports/lmdb 2.8.6-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +580 -0
- package/SECURITY.md +12 -0
- package/bin/download-prebuilds.js +15 -0
- package/binding.gyp +113 -0
- package/build/Release/lmdb.node +0 -0
- package/caching.js +212 -0
- package/dependencies/lmdb/libraries/liblmdb/COPYRIGHT +20 -0
- package/dependencies/lmdb/libraries/liblmdb/Doxyfile +1631 -0
- package/dependencies/lmdb/libraries/liblmdb/LICENSE +47 -0
- package/dependencies/lmdb/libraries/liblmdb/Makefile +136 -0
- package/dependencies/lmdb/libraries/liblmdb/chacha8.c +183 -0
- package/dependencies/lmdb/libraries/liblmdb/chacha8.h +14 -0
- package/dependencies/lmdb/libraries/liblmdb/crypto.c +121 -0
- package/dependencies/lmdb/libraries/liblmdb/intro.doc +192 -0
- package/dependencies/lmdb/libraries/liblmdb/lmdb.h +1817 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb.c +12555 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_copy.1 +74 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_copy.c +106 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_drop.1 +53 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_drop.c +154 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_dump.1 +94 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_dump.c +333 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_load.1 +97 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_load.c +530 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_stat.1 +83 -0
- package/dependencies/lmdb/libraries/liblmdb/mdb_stat.c +276 -0
- package/dependencies/lmdb/libraries/liblmdb/midl-sparse.c +452 -0
- package/dependencies/lmdb/libraries/liblmdb/midl.c +452 -0
- package/dependencies/lmdb/libraries/liblmdb/midl.h +208 -0
- package/dependencies/lmdb/libraries/liblmdb/module.c +101 -0
- package/dependencies/lmdb/libraries/liblmdb/module.h +16 -0
- package/dependencies/lmdb/libraries/liblmdb/mtest.c +178 -0
- package/dependencies/lmdb/libraries/liblmdb/mtest2.c +124 -0
- package/dependencies/lmdb/libraries/liblmdb/mtest3.c +133 -0
- package/dependencies/lmdb/libraries/liblmdb/mtest4.c +168 -0
- package/dependencies/lmdb/libraries/liblmdb/mtest5.c +135 -0
- package/dependencies/lmdb/libraries/liblmdb/mtest6.c +141 -0
- package/dependencies/lmdb/libraries/liblmdb/mtest_enc.c +190 -0
- package/dependencies/lmdb/libraries/liblmdb/mtest_enc2.c +189 -0
- package/dependencies/lmdb/libraries/liblmdb/mtest_remap.c +177 -0
- package/dependencies/lmdb/libraries/liblmdb/sample-bdb.txt +73 -0
- package/dependencies/lmdb/libraries/liblmdb/sample-mdb.txt +62 -0
- package/dependencies/lmdb/libraries/liblmdb/tooltag +27 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/CHANGES +266 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/COPYRIGHT +20 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/Doxyfile +1631 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/LICENSE +47 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/intro.doc +192 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/lmdb.h +1608 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mdb.c +10320 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mdb_copy.1 +55 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mdb_copy.c +82 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mdb_dump.1 +75 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mdb_dump.c +319 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mdb_load.1 +84 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mdb_load.c +496 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mdb_stat.1 +64 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mdb_stat.c +263 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/midl.c +359 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/midl.h +186 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mtest.c +177 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mtest2.c +124 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mtest3.c +133 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mtest4.c +168 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mtest5.c +135 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/mtest6.c +141 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/sample-bdb.txt +73 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/sample-mdb.txt +62 -0
- package/dependencies/lmdb-data-v1/libraries/liblmdb/tooltag +22 -0
- package/dependencies/lz4/LICENSE +11 -0
- package/dependencies/lz4/lib/LICENSE +24 -0
- package/dependencies/lz4/lib/Makefile +225 -0
- package/dependencies/lz4/lib/README.md +169 -0
- package/dependencies/lz4/lib/dll/example/Makefile +63 -0
- package/dependencies/lz4/lib/dll/example/README.md +69 -0
- package/dependencies/lz4/lib/dll/example/fullbench-dll.sln +25 -0
- package/dependencies/lz4/lib/dll/example/fullbench-dll.vcxproj +182 -0
- package/dependencies/lz4/lib/dll/liblz4.def +62 -0
- package/dependencies/lz4/lib/liblz4-dll.rc.in +35 -0
- package/dependencies/lz4/lib/liblz4.pc.in +14 -0
- package/dependencies/lz4/lib/lz4.c +2722 -0
- package/dependencies/lz4/lib/lz4.h +842 -0
- package/dependencies/lz4/lib/lz4file.c +311 -0
- package/dependencies/lz4/lib/lz4file.h +93 -0
- package/dependencies/lz4/lib/lz4frame.c +2078 -0
- package/dependencies/lz4/lib/lz4frame.h +692 -0
- package/dependencies/lz4/lib/lz4frame_static.h +47 -0
- package/dependencies/lz4/lib/lz4hc.c +1631 -0
- package/dependencies/lz4/lib/lz4hc.h +413 -0
- package/dependencies/lz4/lib/xxhash.c +1030 -0
- package/dependencies/lz4/lib/xxhash.h +328 -0
- package/dependencies/v8/v8-fast-api-calls-v16.h +791 -0
- package/dependencies/v8/v8-fast-api-calls.h +888 -0
- package/dict/dict.txt +1 -0
- package/dict/dict2.txt +1 -0
- package/dist/index.cjs +2950 -0
- package/dist/index.cjs.map +1 -0
- package/index.d.cts +420 -0
- package/index.d.ts +450 -0
- package/index.js +38 -0
- package/keys.js +116 -0
- package/level.js +27 -0
- package/native.js +85 -0
- package/node-index.js +32 -0
- package/open.js +448 -0
- package/package.json +124 -0
- package/read.js +833 -0
- package/rollup.config.js +12 -0
- package/src/compression.cpp +225 -0
- package/src/cursor.cpp +395 -0
- package/src/dbi.cpp +382 -0
- package/src/env.cpp +1030 -0
- package/src/lmdb-js.cpp +64 -0
- package/src/lmdb-js.h +633 -0
- package/src/misc.cpp +593 -0
- package/src/ordered-binary.cpp +46 -0
- package/src/txn.cpp +210 -0
- package/src/v8-functions.cpp +170 -0
- package/src/windows.c +35 -0
- package/src/writer.cpp +537 -0
- package/util/RangeIterable.js +236 -0
- package/util/set-optional-deps.cjs +14 -0
- package/util/when.js +8 -0
- package/write.js +912 -0
package/rollup.config.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#include "lz4.h"
|
|
2
|
+
#include "lmdb-js.h"
|
|
3
|
+
#include <atomic>
|
|
4
|
+
|
|
5
|
+
using namespace Napi;
|
|
6
|
+
|
|
7
|
+
thread_local LZ4_stream_t* Compression::stream = nullptr;
|
|
8
|
+
Compression::Compression(const CallbackInfo& info) : ObjectWrap<Compression>(info) {
|
|
9
|
+
unsigned int compressionThreshold = 1000;
|
|
10
|
+
char* dictionary = nullptr;
|
|
11
|
+
size_t dictSize = 0;
|
|
12
|
+
if (info[0].IsObject()) {
|
|
13
|
+
auto dictionaryOption = info[0].As<Object>().Get("dictionary");
|
|
14
|
+
if (!dictionaryOption.IsUndefined()) {
|
|
15
|
+
if (!dictionaryOption.IsTypedArray()) {
|
|
16
|
+
throwError(info.Env(), "Dictionary must be a buffer");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
napi_get_buffer_info(info.Env(), dictionaryOption, (void**) &dictionary, &dictSize);
|
|
20
|
+
dictSize = (dictSize >> 3) << 3; // make sure it is word-aligned
|
|
21
|
+
}
|
|
22
|
+
auto thresholdOption = info[0].As<Object>().Get("threshold");
|
|
23
|
+
if (thresholdOption.IsNumber()) {
|
|
24
|
+
compressionThreshold = thresholdOption.As<Number>();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
this->dictionary = this->compressDictionary = dictionary;
|
|
28
|
+
this->dictionarySize = dictSize;
|
|
29
|
+
this->decompressTarget = dictionary + dictSize;
|
|
30
|
+
this->decompressSize = 0;
|
|
31
|
+
this->acceleration = 1;
|
|
32
|
+
this->compressionThreshold = compressionThreshold;
|
|
33
|
+
info.This().As<Object>().Set("address", Number::New(info.Env(), (double) (size_t) this));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Napi::Value Compression::setBuffer(const CallbackInfo& info) {
|
|
37
|
+
size_t length;
|
|
38
|
+
napi_get_arraybuffer_info(info.Env(), info[0], (void**) &this->decompressTarget, &length);
|
|
39
|
+
unsigned int offset = info[1].As<Number>();
|
|
40
|
+
this->decompressTarget += offset;
|
|
41
|
+
this->decompressSize = info[2].As<Number>();
|
|
42
|
+
this->dictionarySize = info[4].As<Number>();
|
|
43
|
+
napi_get_buffer_info(info.Env(), info[3], (void**) &this->dictionary, &length);
|
|
44
|
+
return info.Env().Undefined();
|
|
45
|
+
}
|
|
46
|
+
void Compression::decompress(MDB_val& data, bool &isValid, bool canAllocate) {
|
|
47
|
+
uint32_t uncompressedLength;
|
|
48
|
+
int compressionHeaderSize;
|
|
49
|
+
uint32_t compressedLength = data.mv_size;
|
|
50
|
+
unsigned char* charData = (unsigned char*) data.mv_data;
|
|
51
|
+
|
|
52
|
+
if (charData[0] == 254) {
|
|
53
|
+
uncompressedLength = ((uint32_t)charData[1] << 16) | ((uint32_t)charData[2] << 8) | (uint32_t)charData[3];
|
|
54
|
+
compressionHeaderSize = 4;
|
|
55
|
+
}
|
|
56
|
+
else if (charData[0] == 255) {
|
|
57
|
+
uncompressedLength = ((uint32_t)charData[4] << 24) | ((uint32_t)charData[5] << 16) | ((uint32_t)charData[6] << 8) | (uint32_t)charData[7];
|
|
58
|
+
compressionHeaderSize = 8;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
fprintf(stderr, "Unknown status byte %u\n", charData[0]);
|
|
62
|
+
//if (canAllocate)
|
|
63
|
+
// Nan::ThrowError("Unknown status byte");
|
|
64
|
+
isValid = false;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
data.mv_data = decompressTarget;
|
|
68
|
+
data.mv_size = uncompressedLength;
|
|
69
|
+
//TODO: For larger blocks with known encoding, it might make sense to allocate space for it and use an ExternalString
|
|
70
|
+
//fprintf(stdout, "compressed size %u uncompressedLength %u, first byte %u\n", data.mv_size, uncompressedLength, charData[compressionHeaderSize]);
|
|
71
|
+
if (uncompressedLength > decompressSize) {
|
|
72
|
+
isValid = false;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
int written = LZ4_decompress_safe_usingDict(
|
|
76
|
+
(char*)charData + compressionHeaderSize, decompressTarget,
|
|
77
|
+
compressedLength - compressionHeaderSize, decompressSize,
|
|
78
|
+
dictionary, dictionarySize);
|
|
79
|
+
//fprintf(stdout, "first uncompressed byte %X %X %X %X %X %X\n", uncompressedData[0], uncompressedData[1], uncompressedData[2], uncompressedData[3], uncompressedData[4], uncompressedData[5]);
|
|
80
|
+
if (written < 0) {
|
|
81
|
+
fprintf(stderr, "Failed to decompress data %u %u bytes:\n", compressionHeaderSize, uncompressedLength);
|
|
82
|
+
for (uint32_t i = 0; i < compressedLength; i++) {
|
|
83
|
+
fprintf(stderr, "%u ", charData[i]);
|
|
84
|
+
}
|
|
85
|
+
//if (canAllocate)
|
|
86
|
+
// Nan::ThrowError("Failed to decompress data");
|
|
87
|
+
isValid = false;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
isValid = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
int Compression::compressInstruction(EnvWrap* env, double* compressionAddress) {
|
|
94
|
+
MDB_val value;
|
|
95
|
+
value.mv_data = (void*)((size_t) * (compressionAddress - 1));
|
|
96
|
+
value.mv_size = *(((uint32_t*)compressionAddress) - 3);
|
|
97
|
+
argtokey_callback_t compressedData = compress(&value, nullptr);
|
|
98
|
+
if (compressedData) {
|
|
99
|
+
*(((uint32_t*)compressionAddress) - 3) = value.mv_size;
|
|
100
|
+
*((size_t*)(compressionAddress - 1)) = (size_t)value.mv_data;
|
|
101
|
+
int64_t status = std::atomic_exchange((std::atomic<int64_t>*) compressionAddress, (int64_t) 0);
|
|
102
|
+
if (status == 1 && env) {
|
|
103
|
+
pthread_mutex_lock(env->writingLock);
|
|
104
|
+
pthread_cond_signal(env->writingCond);
|
|
105
|
+
pthread_mutex_unlock(env->writingLock);
|
|
106
|
+
//fprintf(stderr, "sent compression completion signal\n");
|
|
107
|
+
}
|
|
108
|
+
//fprintf(stdout, "compressed to %p %u %u %p\n", value.mv_data, value.mv_size, status, env);
|
|
109
|
+
return 0;
|
|
110
|
+
} else {
|
|
111
|
+
fprintf(stdout, "failed to compress\n");
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
argtokey_callback_t Compression::compress(MDB_val* value, void (*freeValue)(MDB_val&)) {
|
|
117
|
+
size_t dataLength = value->mv_size;
|
|
118
|
+
char* data = (char*)value->mv_data;
|
|
119
|
+
if (value->mv_size < compressionThreshold && !(value->mv_size > 0 && ((uint8_t*)data)[0] >= 250))
|
|
120
|
+
return freeValue; // don't compress if less than threshold (but we must compress if the first byte is the compression indicator)
|
|
121
|
+
bool longSize = dataLength >= 0x1000000;
|
|
122
|
+
int prefixSize = (longSize ? 8 : 4);
|
|
123
|
+
int maxCompressedSize = LZ4_COMPRESSBOUND(dataLength);
|
|
124
|
+
char* compressed = new char[maxCompressedSize + prefixSize];
|
|
125
|
+
//fprintf(stdout, "compressing %u\n", dataLength);
|
|
126
|
+
if (!stream)
|
|
127
|
+
stream = LZ4_createStream();
|
|
128
|
+
LZ4_loadDict(stream, compressDictionary, dictionarySize);
|
|
129
|
+
int compressedSize = LZ4_compress_fast_continue(stream, data, compressed + prefixSize, dataLength, maxCompressedSize, acceleration);
|
|
130
|
+
if (compressedSize > 0) {
|
|
131
|
+
if (freeValue)
|
|
132
|
+
freeValue(*value);
|
|
133
|
+
uint8_t* compressedData = (uint8_t*)compressed;
|
|
134
|
+
if (longSize) {
|
|
135
|
+
compressedData[0] = 255;
|
|
136
|
+
compressedData[2] = (uint8_t)(dataLength >> 40u);
|
|
137
|
+
compressedData[3] = (uint8_t)(dataLength >> 32u);
|
|
138
|
+
compressedData[4] = (uint8_t)(dataLength >> 24u);
|
|
139
|
+
compressedData[5] = (uint8_t)(dataLength >> 16u);
|
|
140
|
+
compressedData[6] = (uint8_t)(dataLength >> 8u);
|
|
141
|
+
compressedData[7] = (uint8_t)dataLength;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
compressedData[0] = 254;
|
|
145
|
+
compressedData[1] = (uint8_t)(dataLength >> 16u);
|
|
146
|
+
compressedData[2] = (uint8_t)(dataLength >> 8u);
|
|
147
|
+
compressedData[3] = (uint8_t)dataLength;
|
|
148
|
+
}
|
|
149
|
+
value->mv_size = compressedSize + prefixSize;
|
|
150
|
+
value->mv_data = compressed;
|
|
151
|
+
return ([](MDB_val &value) -> void {
|
|
152
|
+
delete[] (char*)value.mv_data;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
delete[] compressed;
|
|
157
|
+
return nullptr;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
class CompressionWorker : public AsyncWorker {
|
|
162
|
+
public:
|
|
163
|
+
CompressionWorker(EnvWrap* env, double* compressionAddress, const Function& callback)
|
|
164
|
+
: AsyncWorker(callback), env(env), compressionAddress(compressionAddress) {}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
void Execute() {
|
|
168
|
+
uint64_t compressionPointer;
|
|
169
|
+
compressionPointer = std::atomic_exchange((std::atomic<int64_t>*) compressionAddress, (int64_t) 2);
|
|
170
|
+
if (compressionPointer > 1) {
|
|
171
|
+
Compression* compression = (Compression*)(size_t) * ((double*)&compressionPointer);
|
|
172
|
+
compression->compressInstruction(env, compressionAddress);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
void OnOK() {
|
|
176
|
+
// don't actually call the callback, no need
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private:
|
|
180
|
+
EnvWrap* env;
|
|
181
|
+
double* compressionAddress;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
NAPI_FUNCTION(EnvWrap::compress) {
|
|
185
|
+
ARGS(3)
|
|
186
|
+
GET_INT64_ARG(0);
|
|
187
|
+
EnvWrap* ew = (EnvWrap*) i64;
|
|
188
|
+
napi_get_value_int64(env, args[1], &i64);
|
|
189
|
+
double* compressionAddress = (double*) i64;
|
|
190
|
+
CompressionWorker* worker = new CompressionWorker(ew, (double*) compressionAddress, Function(env, args[2]));
|
|
191
|
+
worker->Queue();
|
|
192
|
+
RETURN_UNDEFINED;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void Compression::setupExports(Napi::Env env, Object exports) {
|
|
196
|
+
Function CompressionClass = DefineClass(env, "Compression", {
|
|
197
|
+
Compression::InstanceMethod("setBuffer", &Compression::setBuffer),
|
|
198
|
+
});
|
|
199
|
+
exports.Set("Compression", CompressionClass);
|
|
200
|
+
// compressionTpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
// This file contains code from the node-lmdb project
|
|
205
|
+
// Copyright (c) 2013-2017 Timur Kristóf
|
|
206
|
+
// Copyright (c) 2021 Kristopher Tate
|
|
207
|
+
// Licensed to you under the terms of the MIT license
|
|
208
|
+
//
|
|
209
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
210
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
211
|
+
// in the Software without restriction, including without limitation the rights
|
|
212
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
213
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
214
|
+
// furnished to do so, subject to the following conditions:
|
|
215
|
+
|
|
216
|
+
// The above copyright notice and this permission notice shall be included in
|
|
217
|
+
// all copies or substantial portions of the Software.
|
|
218
|
+
|
|
219
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
220
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
221
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
222
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
223
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
224
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
225
|
+
// THE SOFTWARE.
|
package/src/cursor.cpp
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
#include "lmdb-js.h"
|
|
2
|
+
#include <string.h>
|
|
3
|
+
|
|
4
|
+
using namespace Napi;
|
|
5
|
+
|
|
6
|
+
const int INCLUDE_VALUES = 0x100;
|
|
7
|
+
const int REVERSE = 0x400;
|
|
8
|
+
const int VALUES_FOR_KEY = 0x800;
|
|
9
|
+
const int ONLY_COUNT = 0x1000;
|
|
10
|
+
const int RENEW_CURSOR = 0x2000;
|
|
11
|
+
const int EXACT_MATCH = 0x4000;
|
|
12
|
+
const int INCLUSIVE_END = 0x8000;
|
|
13
|
+
const int EXCLUSIVE_START = 0x10000;
|
|
14
|
+
|
|
15
|
+
CursorWrap::CursorWrap(const CallbackInfo& info) : Napi::ObjectWrap<CursorWrap>(info) {
|
|
16
|
+
this->keyType = LmdbKeyType::StringKey;
|
|
17
|
+
this->freeKey = nullptr;
|
|
18
|
+
this->endKey.mv_size = 0; // indicates no end key (yet)
|
|
19
|
+
if (info.Length() < 1) {
|
|
20
|
+
throwError(info.Env(), "Wrong number of arguments");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
DbiWrap *dw;
|
|
25
|
+
napi_unwrap(info.Env(), info[0], (void**)&dw);
|
|
26
|
+
int64_t tw_address = 0;
|
|
27
|
+
napi_get_value_int64(info.Env(), info[1], &tw_address);
|
|
28
|
+
// Open the cursor
|
|
29
|
+
MDB_cursor *cursor;
|
|
30
|
+
MDB_txn *txn = dw->ew->getReadTxn(tw_address);
|
|
31
|
+
int rc = mdb_cursor_open(txn, dw->dbi, &cursor);
|
|
32
|
+
if (rc != 0) {
|
|
33
|
+
throwLmdbError(info.Env(), rc);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
info.This().As<Object>().Set("address", Number::New(info.Env(), (size_t) this));
|
|
37
|
+
this->cursor = cursor;
|
|
38
|
+
this->dw = dw;
|
|
39
|
+
this->txn = txn;
|
|
40
|
+
this->keyType = keyType;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
CursorWrap::~CursorWrap() {
|
|
44
|
+
if (this->cursor) {
|
|
45
|
+
// Don't close cursor here, it is possible that the environment may already be closed, which causes it to crash
|
|
46
|
+
//mdb_cursor_close(this->cursor);
|
|
47
|
+
}
|
|
48
|
+
if (this->freeKey) {
|
|
49
|
+
this->freeKey(this->key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Value CursorWrap::close(const CallbackInfo& info) {
|
|
54
|
+
if (!this->cursor) {
|
|
55
|
+
return throwError(info.Env(), "cursor.close: Attempt to close a closed cursor!");
|
|
56
|
+
}
|
|
57
|
+
mdb_cursor_close(this->cursor);
|
|
58
|
+
this->cursor = nullptr;
|
|
59
|
+
return info.Env().Undefined();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Value CursorWrap::del(const CallbackInfo& info) {
|
|
63
|
+
int flags = 0;
|
|
64
|
+
|
|
65
|
+
if (info.Length() == 1) {
|
|
66
|
+
if (!info[0].IsObject()) {
|
|
67
|
+
return throwError(info.Env(), "cursor.del: Invalid options argument. It should be an object.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
auto options = info[0].As<Object>();
|
|
71
|
+
setFlagFromValue(&flags, MDB_NODUPDATA, "noDupData", false, options);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
int rc = mdb_cursor_del(this->cursor, flags);
|
|
75
|
+
if (rc != 0) {
|
|
76
|
+
return throwLmdbError(info.Env(), rc);
|
|
77
|
+
}
|
|
78
|
+
return info.Env().Undefined();
|
|
79
|
+
}
|
|
80
|
+
int CursorWrap::returnEntry(int lastRC, MDB_val &key, MDB_val &data) {
|
|
81
|
+
if (lastRC) {
|
|
82
|
+
if (lastRC == MDB_NOTFOUND)
|
|
83
|
+
return 0;
|
|
84
|
+
else {
|
|
85
|
+
return lastRC > 0 ? -lastRC : lastRC;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (endKey.mv_size > 0) {
|
|
89
|
+
int comparison;
|
|
90
|
+
if (flags & VALUES_FOR_KEY)
|
|
91
|
+
comparison = mdb_dcmp(txn, dw->dbi, &endKey, &data);
|
|
92
|
+
else
|
|
93
|
+
comparison = mdb_cmp(txn, dw->dbi, &endKey, &key);
|
|
94
|
+
if ((flags & REVERSE) ? comparison >= 0 : (comparison <= 0)) {
|
|
95
|
+
if (!((flags & INCLUSIVE_END) && comparison == 0))
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
char* keyBuffer = dw->ew->keyBuffer;
|
|
100
|
+
if (flags & INCLUDE_VALUES) {
|
|
101
|
+
int result = getVersionAndUncompress(data, dw);
|
|
102
|
+
bool fits = true;
|
|
103
|
+
if (result) {
|
|
104
|
+
fits = valToBinaryFast(data, dw); // it fit in the global/compression-target buffer
|
|
105
|
+
}
|
|
106
|
+
if (fits || result == 2 || data.mv_size < SHARED_BUFFER_THRESHOLD) {// if it was decompressed
|
|
107
|
+
*((uint32_t*)keyBuffer) = data.mv_size;
|
|
108
|
+
*((uint32_t*)(keyBuffer + 4)) = 0; // buffer id of 0
|
|
109
|
+
} else {
|
|
110
|
+
EnvWrap::toSharedBuffer(dw->ew->env, (uint32_t*) dw->ew->keyBuffer, data);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!(flags & VALUES_FOR_KEY)) {
|
|
114
|
+
memcpy(keyBuffer + 32, key.mv_data, key.mv_size);
|
|
115
|
+
*(keyBuffer + 32 + key.mv_size) = 0; // make sure it is null terminated for the sake of better ordered-binary performance
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return key.mv_size;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const int START_ADDRESS_POSITION = 4064;
|
|
122
|
+
int32_t CursorWrap::doPosition(uint32_t offset, uint32_t keySize, uint64_t endKeyAddress) {
|
|
123
|
+
//char* keyBuffer = dw->ew->keyBuffer;
|
|
124
|
+
MDB_val key, data;
|
|
125
|
+
int rc;
|
|
126
|
+
if (dw->ew->env == nullptr) {
|
|
127
|
+
return MDB_BAD_TXN;
|
|
128
|
+
}
|
|
129
|
+
if (flags & RENEW_CURSOR) { // TODO: check the txn_id to determine if we need to renew
|
|
130
|
+
rc = mdb_cursor_renew(txn = dw->ew->getReadTxn(), cursor);
|
|
131
|
+
if (rc) {
|
|
132
|
+
if (rc > 0)
|
|
133
|
+
rc = -rc;
|
|
134
|
+
return rc;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (endKeyAddress) {
|
|
138
|
+
uint32_t* keyBuffer = (uint32_t*) endKeyAddress;
|
|
139
|
+
endKey.mv_size = *keyBuffer;
|
|
140
|
+
endKey.mv_data = (char*)(keyBuffer + 1);
|
|
141
|
+
} else
|
|
142
|
+
endKey.mv_size = 0;
|
|
143
|
+
iteratingOp = (flags & REVERSE) ?
|
|
144
|
+
(flags & INCLUDE_VALUES) ?
|
|
145
|
+
(flags & VALUES_FOR_KEY) ? MDB_PREV_DUP : MDB_PREV :
|
|
146
|
+
MDB_PREV_NODUP :
|
|
147
|
+
(flags & INCLUDE_VALUES) ?
|
|
148
|
+
(flags & VALUES_FOR_KEY) ? MDB_NEXT_DUP : MDB_NEXT :
|
|
149
|
+
MDB_NEXT_NODUP;
|
|
150
|
+
key.mv_size = keySize;
|
|
151
|
+
key.mv_data = dw->ew->keyBuffer;
|
|
152
|
+
if (keySize == 0) {
|
|
153
|
+
rc = mdb_cursor_get(cursor, &key, &data, flags & REVERSE ? MDB_LAST : MDB_FIRST);
|
|
154
|
+
} else {
|
|
155
|
+
if (flags & VALUES_FOR_KEY) { // only values for this key
|
|
156
|
+
// take the next part of the key buffer as a pointer to starting data
|
|
157
|
+
uint32_t* startValueBuffer = (uint32_t*)(size_t)(*(double*)(dw->ew->keyBuffer + START_ADDRESS_POSITION));
|
|
158
|
+
data.mv_size = endKeyAddress ? *((uint32_t*)startValueBuffer) : 0;
|
|
159
|
+
data.mv_data = startValueBuffer + 1;
|
|
160
|
+
MDB_val startValue;
|
|
161
|
+
if (flags & EXCLUSIVE_START)
|
|
162
|
+
startValue = data; // save it for comparison
|
|
163
|
+
if (flags & REVERSE) {// reverse through values
|
|
164
|
+
startValue = data; // save it for comparison
|
|
165
|
+
rc = mdb_cursor_get(cursor, &key, &data, data.mv_size ? MDB_GET_BOTH_RANGE : MDB_SET_KEY);
|
|
166
|
+
if (rc) {
|
|
167
|
+
if (startValue.mv_size) {
|
|
168
|
+
// value specified, but not found, so find key and go to last item
|
|
169
|
+
rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_KEY);
|
|
170
|
+
if (!rc)
|
|
171
|
+
rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP);
|
|
172
|
+
} // else just couldn't find the key
|
|
173
|
+
} else { // found entry
|
|
174
|
+
if (startValue.mv_size == 0) // no value specified, so go to last value
|
|
175
|
+
rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP);
|
|
176
|
+
else if (mdb_dcmp(txn, dw->dbi, &startValue, &data)) // the range found the next value *after* the start
|
|
177
|
+
rc = mdb_cursor_get(cursor, &key, &data, MDB_PREV_DUP);
|
|
178
|
+
}
|
|
179
|
+
} else // forward, just do a get by range
|
|
180
|
+
rc = mdb_cursor_get(cursor, &key, &data, data.mv_size ?
|
|
181
|
+
(flags & EXACT_MATCH) ? MDB_GET_BOTH : MDB_GET_BOTH_RANGE : MDB_SET_KEY);
|
|
182
|
+
|
|
183
|
+
if (rc == MDB_NOTFOUND)
|
|
184
|
+
return 0;
|
|
185
|
+
if (flags & ONLY_COUNT && (!endKeyAddress || (flags & EXACT_MATCH))) {
|
|
186
|
+
size_t count;
|
|
187
|
+
rc = mdb_cursor_count(cursor, &count);
|
|
188
|
+
if (rc)
|
|
189
|
+
return rc > 0 ? -rc : rc;
|
|
190
|
+
return count;
|
|
191
|
+
}
|
|
192
|
+
if (flags & EXCLUSIVE_START) {
|
|
193
|
+
while(!rc) {
|
|
194
|
+
if (mdb_dcmp(txn, dw->dbi, &startValue, &data))
|
|
195
|
+
break;
|
|
196
|
+
rc = mdb_cursor_get(cursor, &key, &data, iteratingOp);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
MDB_val firstKey;
|
|
201
|
+
if (flags & EXCLUSIVE_START)
|
|
202
|
+
firstKey = key; // save it for comparison
|
|
203
|
+
if (flags & REVERSE) {// reverse
|
|
204
|
+
firstKey = key; // save it for comparison
|
|
205
|
+
rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE);
|
|
206
|
+
if (rc)
|
|
207
|
+
rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST);
|
|
208
|
+
else if (mdb_cmp(txn, dw->dbi, &firstKey, &key)) // the range found the next entry *after* the start
|
|
209
|
+
rc = mdb_cursor_get(cursor, &key, &data, MDB_PREV);
|
|
210
|
+
else if (dw->flags & MDB_DUPSORT)
|
|
211
|
+
// we need to go to the last value of this key
|
|
212
|
+
rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP);
|
|
213
|
+
} else // forward, just do a get by range
|
|
214
|
+
rc = mdb_cursor_get(cursor, &key, &data, (flags & EXACT_MATCH) ? MDB_SET_KEY : MDB_SET_RANGE);
|
|
215
|
+
if (flags & EXCLUSIVE_START) {
|
|
216
|
+
while(!rc) {
|
|
217
|
+
if (mdb_cmp(txn, dw->dbi, &firstKey, &key))
|
|
218
|
+
break;
|
|
219
|
+
rc = mdb_cursor_get(cursor, &key, &data, iteratingOp);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
while (offset-- > 0 && !rc) {
|
|
226
|
+
rc = mdb_cursor_get(cursor, &key, &data, iteratingOp);
|
|
227
|
+
}
|
|
228
|
+
if (flags & ONLY_COUNT) {
|
|
229
|
+
uint32_t count = 0;
|
|
230
|
+
bool useCursorCount = false;
|
|
231
|
+
// if we are in a dupsort database, and we are iterating over all entries, we can just count all the values for each key
|
|
232
|
+
if (dw->flags & MDB_DUPSORT) {
|
|
233
|
+
if (iteratingOp == MDB_PREV) {
|
|
234
|
+
iteratingOp = MDB_PREV_NODUP;
|
|
235
|
+
useCursorCount = true;
|
|
236
|
+
}
|
|
237
|
+
if (iteratingOp == MDB_NEXT) {
|
|
238
|
+
iteratingOp = MDB_NEXT_NODUP;
|
|
239
|
+
useCursorCount = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
while (!rc) {
|
|
244
|
+
if (endKey.mv_size > 0) {
|
|
245
|
+
int comparison;
|
|
246
|
+
if (flags & VALUES_FOR_KEY)
|
|
247
|
+
comparison = mdb_dcmp(txn, dw->dbi, &endKey, &data);
|
|
248
|
+
else
|
|
249
|
+
comparison = mdb_cmp(txn, dw->dbi, &endKey, &key);
|
|
250
|
+
if ((flags & REVERSE) ? comparison >= 0 : (comparison <=0)) {
|
|
251
|
+
if (!((flags & INCLUSIVE_END) && comparison == 0))
|
|
252
|
+
return count;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (useCursorCount) {
|
|
256
|
+
size_t countForKey;
|
|
257
|
+
rc = mdb_cursor_count(cursor, &countForKey);
|
|
258
|
+
if (rc) {
|
|
259
|
+
if (rc > 0)
|
|
260
|
+
rc = -rc;
|
|
261
|
+
return rc;
|
|
262
|
+
}
|
|
263
|
+
count += countForKey;
|
|
264
|
+
} else
|
|
265
|
+
count++;
|
|
266
|
+
rc = mdb_cursor_get(cursor, &key, &data, iteratingOp);
|
|
267
|
+
}
|
|
268
|
+
return count;
|
|
269
|
+
}
|
|
270
|
+
// TODO: Handle count?
|
|
271
|
+
return returnEntry(rc, key, data);
|
|
272
|
+
}
|
|
273
|
+
NAPI_FUNCTION(position) {
|
|
274
|
+
ARGS(5)
|
|
275
|
+
GET_INT64_ARG(0);
|
|
276
|
+
CursorWrap* cw = (CursorWrap*) i64;
|
|
277
|
+
GET_UINT32_ARG(cw->flags, 1);
|
|
278
|
+
uint32_t offset;
|
|
279
|
+
GET_UINT32_ARG(offset, 2);
|
|
280
|
+
uint32_t keySize;
|
|
281
|
+
GET_UINT32_ARG(keySize, 3);
|
|
282
|
+
napi_get_value_int64(env, args[4], &i64);
|
|
283
|
+
int64_t endKeyAddress = i64;
|
|
284
|
+
int32_t result = cw->doPosition(offset, keySize, endKeyAddress);
|
|
285
|
+
RETURN_INT32(result);
|
|
286
|
+
}
|
|
287
|
+
int32_t positionFFI(double cwPointer, uint32_t flags, uint32_t offset, uint32_t keySize, uint64_t endKeyAddress) {
|
|
288
|
+
CursorWrap* cw = (CursorWrap*) (size_t) cwPointer;
|
|
289
|
+
DbiWrap* dw = cw->dw;
|
|
290
|
+
dw->getFast = true;
|
|
291
|
+
cw->flags = flags;
|
|
292
|
+
return cw->doPosition(offset, keySize, endKeyAddress);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
NAPI_FUNCTION(iterate) {
|
|
296
|
+
ARGS(1)
|
|
297
|
+
GET_INT64_ARG(0);
|
|
298
|
+
CursorWrap* cw = (CursorWrap*) i64;
|
|
299
|
+
MDB_val key, data;
|
|
300
|
+
int rc;
|
|
301
|
+
if (cw->dw->ew->env == nullptr) rc = MDB_BAD_TXN;
|
|
302
|
+
else
|
|
303
|
+
rc = mdb_cursor_get(cw->cursor, &key, &data, cw->iteratingOp);
|
|
304
|
+
RETURN_INT32(cw->returnEntry(rc, key, data));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
int32_t iterateFFI(double cwPointer) {
|
|
308
|
+
CursorWrap* cw = (CursorWrap*) (size_t) cwPointer;
|
|
309
|
+
DbiWrap* dw = cw->dw;
|
|
310
|
+
dw->getFast = true;
|
|
311
|
+
MDB_val key, data;
|
|
312
|
+
if (cw->dw->ew->env == nullptr)
|
|
313
|
+
return MDB_BAD_TXN;
|
|
314
|
+
int rc = mdb_cursor_get(cw->cursor, &key, &data, cw->iteratingOp);
|
|
315
|
+
return cw->returnEntry(rc, key, data);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
NAPI_FUNCTION(getCurrentValue) {
|
|
320
|
+
ARGS(1)
|
|
321
|
+
GET_INT64_ARG(0);
|
|
322
|
+
CursorWrap* cw = (CursorWrap*) i64;
|
|
323
|
+
MDB_val key, data;
|
|
324
|
+
int rc = mdb_cursor_get(cw->cursor, &key, &data, MDB_GET_CURRENT);
|
|
325
|
+
RETURN_INT32(cw->returnEntry(rc, key, data));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
napi_finalize noopCursor = [](napi_env, void *, void *) {
|
|
329
|
+
// Data belongs to LMDB, we shouldn't free it here
|
|
330
|
+
};
|
|
331
|
+
NAPI_FUNCTION(getCurrentShared) {
|
|
332
|
+
ARGS(1)
|
|
333
|
+
GET_INT64_ARG(0);
|
|
334
|
+
CursorWrap* cw = (CursorWrap*) i64;
|
|
335
|
+
MDB_val key, data;
|
|
336
|
+
int rc = mdb_cursor_get(cw->cursor, &key, &data, MDB_GET_CURRENT);
|
|
337
|
+
if (rc)
|
|
338
|
+
RETURN_INT32(cw->returnEntry(rc, key, data));
|
|
339
|
+
getVersionAndUncompress(data, cw->dw);
|
|
340
|
+
napi_create_external_buffer(env, data.mv_size,
|
|
341
|
+
(char*) data.mv_data, noopCursor, nullptr, &returnValue);
|
|
342
|
+
return returnValue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
NAPI_FUNCTION(renew) {
|
|
346
|
+
ARGS(1)
|
|
347
|
+
GET_INT64_ARG(0);
|
|
348
|
+
CursorWrap* cw = (CursorWrap*) i64;
|
|
349
|
+
mdb_cursor_renew(cw->txn = cw->dw->ew->getReadTxn(), cw->cursor);
|
|
350
|
+
RETURN_UNDEFINED;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
void CursorWrap::setupExports(Napi::Env env, Object exports) {
|
|
354
|
+
// CursorWrap: Prepare constructor template
|
|
355
|
+
Function CursorClass = DefineClass(env, "Cursor", {
|
|
356
|
+
// CursorWrap: Add functions to the prototype
|
|
357
|
+
CursorWrap::InstanceMethod("close", &CursorWrap::close),
|
|
358
|
+
CursorWrap::InstanceMethod("del", &CursorWrap::del),
|
|
359
|
+
});
|
|
360
|
+
EXPORT_NAPI_FUNCTION("position", position);
|
|
361
|
+
EXPORT_NAPI_FUNCTION("iterate", iterate);
|
|
362
|
+
EXPORT_NAPI_FUNCTION("getCurrentValue", getCurrentValue);
|
|
363
|
+
EXPORT_NAPI_FUNCTION("getCurrentShared", getCurrentShared);
|
|
364
|
+
EXPORT_NAPI_FUNCTION("renew", renew);
|
|
365
|
+
EXPORT_FUNCTION_ADDRESS("positionPtr", positionFFI);
|
|
366
|
+
EXPORT_FUNCTION_ADDRESS("iteratePtr", iterateFFI);
|
|
367
|
+
|
|
368
|
+
exports.Set("Cursor", CursorClass);
|
|
369
|
+
|
|
370
|
+
// cursorTpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// This file contains code from the node-lmdb project
|
|
374
|
+
// Copyright (c) 2013-2017 Timur Kristóf
|
|
375
|
+
// Copyright (c) 2021 Kristopher Tate
|
|
376
|
+
// Licensed to you under the terms of the MIT license
|
|
377
|
+
//
|
|
378
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
379
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
380
|
+
// in the Software without restriction, including without limitation the rights
|
|
381
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
382
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
383
|
+
// furnished to do so, subject to the following conditions:
|
|
384
|
+
|
|
385
|
+
// The above copyright notice and this permission notice shall be included in
|
|
386
|
+
// all copies or substantial portions of the Software.
|
|
387
|
+
|
|
388
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
389
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
390
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
391
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
392
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
393
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
394
|
+
// THE SOFTWARE.
|
|
395
|
+
|