qvdjs 0.9.0 → 0.9.2
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/dist/index.cjs +256 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +254 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
5
|
var crypto = require('crypto');
|
|
6
6
|
var xml2 = require('xml2js');
|
|
7
7
|
var assert = require('assert');
|
|
@@ -10,8 +10,8 @@ var v8 = require('v8');
|
|
|
10
10
|
|
|
11
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
12
|
|
|
13
|
-
var
|
|
14
|
-
var
|
|
13
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
14
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
15
15
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
16
16
|
var xml2__default = /*#__PURE__*/_interopDefault(xml2);
|
|
17
17
|
var assert__default = /*#__PURE__*/_interopDefault(assert);
|
|
@@ -262,25 +262,101 @@ var init_QvdSymbol = __esm({
|
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
});
|
|
265
|
+
function isWithinDirectoryLexically(resolvedBaseDir, resolvedPath) {
|
|
266
|
+
const isCaseInsensitiveFS = process.platform === "win32";
|
|
267
|
+
const base = isCaseInsensitiveFS ? resolvedBaseDir.toLowerCase() : resolvedBaseDir;
|
|
268
|
+
const target = isCaseInsensitiveFS ? resolvedPath.toLowerCase() : resolvedPath;
|
|
269
|
+
const relative = path__default.default.relative(base, target);
|
|
270
|
+
if (relative === "") {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
if (path__default.default.isAbsolute(relative)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
return relative !== ".." && !relative.startsWith(`..${path__default.default.sep}`);
|
|
277
|
+
}
|
|
278
|
+
function resolveDeepestExisting(target) {
|
|
279
|
+
let current = target;
|
|
280
|
+
for (; ; ) {
|
|
281
|
+
try {
|
|
282
|
+
return fs__default.default.realpathSync(current);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const code = (
|
|
285
|
+
/** @type {{code?: string}} */
|
|
286
|
+
error?.code
|
|
287
|
+
);
|
|
288
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const parent = path__default.default.dirname(current);
|
|
292
|
+
if (parent === current) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
current = parent;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function isWithinDirectoryOnDisk(resolvedBaseDir, resolvedPath) {
|
|
300
|
+
let baseStat;
|
|
301
|
+
try {
|
|
302
|
+
baseStat = fs__default.default.statSync(fs__default.default.realpathSync(resolvedBaseDir));
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
let current = resolveDeepestExisting(resolvedPath);
|
|
307
|
+
if (current === null) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
for (; ; ) {
|
|
311
|
+
let stat;
|
|
312
|
+
try {
|
|
313
|
+
stat = fs__default.default.statSync(current);
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (stat.dev === baseStat.dev && stat.ino === baseStat.ino) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
const parent = path__default.default.dirname(current);
|
|
321
|
+
if (parent === current) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
current = parent;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
265
327
|
function validatePath(filePath, allowedDir) {
|
|
328
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
329
|
+
throw new exports.QvdValidationError("filePath must be a non-empty string", {
|
|
330
|
+
provided: filePath,
|
|
331
|
+
type: typeof filePath
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (allowedDir !== void 0 && allowedDir !== null && typeof allowedDir !== "string") {
|
|
335
|
+
throw new exports.QvdValidationError("allowedDir must be a string, null or undefined", {
|
|
336
|
+
provided: allowedDir,
|
|
337
|
+
type: typeof allowedDir
|
|
338
|
+
});
|
|
339
|
+
}
|
|
266
340
|
if (filePath.includes("\0")) {
|
|
267
341
|
throw new exports.QvdSecurityError("Path traversal detected: Null byte in path", {
|
|
268
342
|
path: filePath,
|
|
269
343
|
reason: "null_byte"
|
|
270
344
|
});
|
|
271
345
|
}
|
|
346
|
+
const resolvedPath = path__default.default.resolve(filePath);
|
|
272
347
|
const baseDir = allowedDir || process.cwd();
|
|
273
|
-
const resolvedBaseDir =
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
const pathForComparison = isCaseInsensitiveFS ? resolvedPath.toLowerCase() : resolvedPath;
|
|
278
|
-
if (!pathForComparison.startsWith(baseForComparison + path2__default.default.sep) && pathForComparison !== baseForComparison) {
|
|
348
|
+
const resolvedBaseDir = path__default.default.resolve(baseDir);
|
|
349
|
+
const onDisk = isWithinDirectoryOnDisk(resolvedBaseDir, resolvedPath);
|
|
350
|
+
const contained = onDisk === null ? isWithinDirectoryLexically(resolvedBaseDir, resolvedPath) : onDisk;
|
|
351
|
+
if (!contained) {
|
|
279
352
|
throw new exports.QvdSecurityError("Path traversal detected: Access denied", {
|
|
280
353
|
path: filePath,
|
|
281
354
|
resolvedPath,
|
|
282
355
|
allowedDir: resolvedBaseDir,
|
|
283
|
-
reason: "outside_allowed_directory"
|
|
356
|
+
reason: "outside_allowed_directory",
|
|
357
|
+
// Says which check refused, so a rejection of a path that looks contained is traceable to
|
|
358
|
+
// a symlink or a case difference rather than looking like a bug.
|
|
359
|
+
check: onDisk === null ? "lexical" : "filesystem"
|
|
284
360
|
});
|
|
285
361
|
}
|
|
286
362
|
return resolvedPath;
|
|
@@ -309,7 +385,10 @@ var init_QvdFileWriter = __esm({
|
|
|
309
385
|
* @param {QvdDataFrame} df The data frame to write to the QVD file.
|
|
310
386
|
* @param {Object} [options={}] Options for the writer.
|
|
311
387
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
312
|
-
* path must be within this directory
|
|
388
|
+
* path must be within this directory, with symlinks resolved first, so a link inside it that
|
|
389
|
+
* points outside it is rejected. Defaults to the current working directory. To permit
|
|
390
|
+
* an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
|
|
391
|
+
* empty value falls back to the working directory rather than removing the restriction.
|
|
313
392
|
* @param {Function} [options.onProgress] Optional progress callback function.
|
|
314
393
|
*/
|
|
315
394
|
constructor(filePath, df, options = {}) {
|
|
@@ -355,7 +434,7 @@ var init_QvdFileWriter = __esm({
|
|
|
355
434
|
const headerBuffer = Buffer.concat([Buffer.from(this._header, "utf-8"), Buffer.from([0])]);
|
|
356
435
|
let fd;
|
|
357
436
|
try {
|
|
358
|
-
fd = await
|
|
437
|
+
fd = await fs__default.default.promises.open(this._path, "w");
|
|
359
438
|
await fd.write(headerBuffer, 0, headerBuffer.length, 0);
|
|
360
439
|
await fd.write(this._symbolBuffer, 0, this._symbolBuffer.length, headerBuffer.length);
|
|
361
440
|
await fd.write(this._indexBuffer, 0, this._indexBuffer.length, headerBuffer.length + this._symbolBuffer.length);
|
|
@@ -381,7 +460,7 @@ var init_QvdFileWriter = __esm({
|
|
|
381
460
|
SourceFileUtcTime: existingMetadata.SourceFileUtcTime || "",
|
|
382
461
|
SourceFileSize: existingMetadata.SourceFileSize || -1,
|
|
383
462
|
StaleUtcTime: existingMetadata.StaleUtcTime || "",
|
|
384
|
-
TableName: existingMetadata.TableName ||
|
|
463
|
+
TableName: existingMetadata.TableName || path__default.default.basename(this._path, path__default.default.extname(this._path)),
|
|
385
464
|
Compression: existingMetadata.Compression || "",
|
|
386
465
|
Comment: existingMetadata.Comment || "",
|
|
387
466
|
EncryptionInfo: existingMetadata.EncryptionInfo || "",
|
|
@@ -401,7 +480,7 @@ var init_QvdFileWriter = __esm({
|
|
|
401
480
|
SourceFileUtcTime: "",
|
|
402
481
|
SourceFileSize: -1,
|
|
403
482
|
StaleUtcTime: "",
|
|
404
|
-
TableName:
|
|
483
|
+
TableName: path__default.default.basename(this._path, path__default.default.extname(this._path)),
|
|
405
484
|
Compression: "",
|
|
406
485
|
Comment: "",
|
|
407
486
|
EncryptionInfo: "",
|
|
@@ -571,21 +650,22 @@ var init_QvdFileWriter = __esm({
|
|
|
571
650
|
this._emitProgress("index-table", processedRows, numRows);
|
|
572
651
|
}
|
|
573
652
|
});
|
|
574
|
-
this._df.columns.forEach((column) => {
|
|
575
|
-
const bitOffset = this._indexTableMetadata?.slice(0,
|
|
653
|
+
this._df.columns.forEach((column, columnIndex) => {
|
|
654
|
+
const bitOffset = this._indexTableMetadata?.slice(0, columnIndex).reduce((sum, metadata) => sum + metadata[1], 0);
|
|
576
655
|
assert__default.default(this._indexTable, "The QVD file header has not been parsed.");
|
|
577
|
-
const
|
|
578
|
-
...this._indexTable.map(
|
|
579
|
-
(indices) => indices[this._df.columns.indexOf(column)].length
|
|
580
|
-
)
|
|
581
|
-
) : 0;
|
|
582
|
-
const fieldContainsNull = this._symbolTableMetadata?.[this._df.columns.indexOf(column)][2];
|
|
656
|
+
const fieldContainsNull = this._symbolTableMetadata?.[columnIndex][2];
|
|
583
657
|
const bias = fieldContainsNull ? -2 : 0;
|
|
658
|
+
const symbolCount = this._symbolTable?.[columnIndex].length ?? 0;
|
|
659
|
+
const maxStoredIndex = symbolCount === 0 ? 0 : symbolCount - 1 + (fieldContainsNull ? 2 : 0);
|
|
660
|
+
const bitWidth = maxStoredIndex === 0 ? 0 : 32 - Math.clz32(maxStoredIndex);
|
|
584
661
|
this._indexTableMetadata?.push([bitOffset, bitWidth, bias]);
|
|
585
662
|
this._indexTable.forEach((indices) => {
|
|
586
|
-
const bitString = indices[
|
|
587
|
-
|
|
588
|
-
|
|
663
|
+
const bitString = indices[columnIndex];
|
|
664
|
+
assert__default.default(
|
|
665
|
+
bitString === "0" || bitString.length <= bitWidth,
|
|
666
|
+
`Stored index for column '${column}' needs ${bitString.length} bits, but BitWidth was derived as ${bitWidth} from ${symbolCount} symbols. The symbol table and the index table are out of sync.`
|
|
667
|
+
);
|
|
668
|
+
indices[columnIndex] = bitWidth === 0 ? "" : bitString.padStart(bitWidth, "0");
|
|
589
669
|
});
|
|
590
670
|
});
|
|
591
671
|
this._indexBuffer = Buffer.concat(
|
|
@@ -593,6 +673,9 @@ var init_QvdFileWriter = __esm({
|
|
|
593
673
|
this._indexTable.map((indices) => {
|
|
594
674
|
indices.reverse();
|
|
595
675
|
const bits = indices.join("");
|
|
676
|
+
if (bits.length === 0) {
|
|
677
|
+
return Buffer.from(Uint8Array.from([0]));
|
|
678
|
+
}
|
|
596
679
|
const paddingWidth = (8 - bits.length % 8) % 8;
|
|
597
680
|
const paddedBits = bits.padStart(bits.length + paddingWidth, "0");
|
|
598
681
|
const bytes = paddedBits.match(/.{1,8}/g)?.map((byte) => parseInt(byte, 2));
|
|
@@ -805,7 +888,7 @@ function validateFieldMetadata(field, symbolBufferLength, filePath) {
|
|
|
805
888
|
});
|
|
806
889
|
}
|
|
807
890
|
}
|
|
808
|
-
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath) {
|
|
891
|
+
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath, fileSize = null) {
|
|
809
892
|
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
810
893
|
throw new exports.QvdCorruptedError("Invalid record byte size", {
|
|
811
894
|
recordSize,
|
|
@@ -851,16 +934,40 @@ function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, ind
|
|
|
851
934
|
stage: "parseIndexTable"
|
|
852
935
|
});
|
|
853
936
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
throw new exports.QvdCorruptedError("Index table length unreasonably large", {
|
|
937
|
+
if (indexTableLength !== totalRows * recordSize) {
|
|
938
|
+
throw new exports.QvdCorruptedError("Index table length inconsistent with record count", {
|
|
857
939
|
indexTableLength,
|
|
858
|
-
|
|
940
|
+
expectedLength: totalRows * recordSize,
|
|
941
|
+
totalRows,
|
|
942
|
+
recordSize,
|
|
859
943
|
file: filePath,
|
|
860
944
|
stage: "parseIndexTable"
|
|
861
945
|
});
|
|
862
946
|
}
|
|
947
|
+
if (fileSize !== null) {
|
|
948
|
+
if (indexTableOffset + indexTableLength > fileSize) {
|
|
949
|
+
throw new exports.QvdCorruptedError("Index table extends beyond the end of the file", {
|
|
950
|
+
indexTableOffset,
|
|
951
|
+
indexTableLength,
|
|
952
|
+
fileSize,
|
|
953
|
+
file: filePath,
|
|
954
|
+
stage: "parseIndexTable"
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
863
958
|
const requiredIndexBytes = rowsToLoad * recordSize;
|
|
959
|
+
if (indexTableOffset + requiredIndexBytes > bufferLength) {
|
|
960
|
+
throw new exports.QvdCorruptedError("Index table truncated", {
|
|
961
|
+
indexTableOffset,
|
|
962
|
+
requiredBytes: requiredIndexBytes,
|
|
963
|
+
availableBytes: Math.max(0, bufferLength - indexTableOffset),
|
|
964
|
+
rowsToLoad,
|
|
965
|
+
recordSize,
|
|
966
|
+
bufferSize: bufferLength,
|
|
967
|
+
file: filePath,
|
|
968
|
+
stage: "parseIndexTable"
|
|
969
|
+
});
|
|
970
|
+
}
|
|
864
971
|
if (indexTableLength < requiredIndexBytes) {
|
|
865
972
|
throw new exports.QvdCorruptedError("Index table length smaller than required", {
|
|
866
973
|
indexTableLength,
|
|
@@ -1171,7 +1278,7 @@ var QvdFileReader_exports = {};
|
|
|
1171
1278
|
__export(QvdFileReader_exports, {
|
|
1172
1279
|
QvdFileReader: () => exports.QvdFileReader
|
|
1173
1280
|
});
|
|
1174
|
-
exports.QvdFileReader = void 0;
|
|
1281
|
+
var MAX_HEADER_SIZE, READ_CHUNK_SIZE; exports.QvdFileReader = void 0;
|
|
1175
1282
|
var init_QvdFileReader = __esm({
|
|
1176
1283
|
"src/QvdFileReader.js"() {
|
|
1177
1284
|
init_QvdDataFrame();
|
|
@@ -1181,6 +1288,8 @@ var init_QvdFileReader = __esm({
|
|
|
1181
1288
|
init_memoryUtils();
|
|
1182
1289
|
init_validationUtils();
|
|
1183
1290
|
init_symbolParser();
|
|
1291
|
+
MAX_HEADER_SIZE = 16 * 1024 * 1024;
|
|
1292
|
+
READ_CHUNK_SIZE = 512 * 1024 * 1024;
|
|
1184
1293
|
exports.QvdFileReader = class {
|
|
1185
1294
|
/**
|
|
1186
1295
|
* Constructs a new QVD file parser.
|
|
@@ -1188,7 +1297,10 @@ var init_QvdFileReader = __esm({
|
|
|
1188
1297
|
* @param {string} filePath The path to the QVD file to load.
|
|
1189
1298
|
* @param {Object} [options={}] Options for the reader.
|
|
1190
1299
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
1191
|
-
* path must be within this directory
|
|
1300
|
+
* path must be within this directory, with symlinks resolved first, so a link inside it that
|
|
1301
|
+
* points outside it is rejected. Defaults to the current working directory. To permit
|
|
1302
|
+
* an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
|
|
1303
|
+
* empty value falls back to the working directory rather than removing the restriction.
|
|
1192
1304
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
|
|
1193
1305
|
* what percentage of available memory (or V8 heap limit, whichever is smaller) can be used
|
|
1194
1306
|
* for loading QVD files. Default is 0.3 (30%). Increase for larger heap configurations.
|
|
@@ -1204,6 +1316,7 @@ var init_QvdFileReader = __esm({
|
|
|
1204
1316
|
this._header = null;
|
|
1205
1317
|
this._symbolTable = null;
|
|
1206
1318
|
this._indexTable = null;
|
|
1319
|
+
this._fileSize = null;
|
|
1207
1320
|
}
|
|
1208
1321
|
/**
|
|
1209
1322
|
* Reads the binary data of the QVD file.
|
|
@@ -1234,27 +1347,50 @@ var init_QvdFileReader = __esm({
|
|
|
1234
1347
|
*/
|
|
1235
1348
|
async _readData(maxRows = null) {
|
|
1236
1349
|
if (maxRows === null) {
|
|
1237
|
-
this._buffer = await
|
|
1350
|
+
this._buffer = await fs__default.default.promises.readFile(this._path);
|
|
1351
|
+
this._fileSize = this._buffer.length;
|
|
1238
1352
|
return;
|
|
1239
1353
|
}
|
|
1240
1354
|
const HEADER_DELIMITER = "\r\n\0";
|
|
1241
1355
|
const CHUNK_SIZE = 64 * 1024;
|
|
1242
|
-
const stream =
|
|
1356
|
+
const stream = fs__default.default.createReadStream(this._path, {
|
|
1243
1357
|
highWaterMark: CHUNK_SIZE
|
|
1244
1358
|
});
|
|
1359
|
+
const headerChunks = [];
|
|
1360
|
+
let headerBytes = 0;
|
|
1361
|
+
let tail = Buffer.alloc(0);
|
|
1245
1362
|
let headerBuffer = Buffer.alloc(0);
|
|
1246
1363
|
let headerDelimiterIndex = -1;
|
|
1247
1364
|
try {
|
|
1248
1365
|
for await (const chunk of stream) {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1366
|
+
const chunkStart = headerBytes;
|
|
1367
|
+
const searchBuffer = tail.length > 0 ? Buffer.concat([tail, chunk]) : chunk;
|
|
1368
|
+
const foundInSearch = searchBuffer.indexOf(HEADER_DELIMITER);
|
|
1369
|
+
headerChunks.push(chunk);
|
|
1370
|
+
headerBytes += chunk.length;
|
|
1371
|
+
if (foundInSearch !== -1) {
|
|
1372
|
+
headerDelimiterIndex = chunkStart - tail.length + foundInSearch;
|
|
1252
1373
|
stream.destroy();
|
|
1253
1374
|
break;
|
|
1254
1375
|
}
|
|
1376
|
+
tail = Buffer.from(searchBuffer.subarray(-(HEADER_DELIMITER.length - 1)));
|
|
1377
|
+
if (headerBytes > MAX_HEADER_SIZE) {
|
|
1378
|
+
stream.destroy();
|
|
1379
|
+
throw new exports.QvdCorruptedError(
|
|
1380
|
+
`The XML header delimiter was not found within the first ${MAX_HEADER_SIZE / (1024 * 1024)}MB of the file.`,
|
|
1381
|
+
{
|
|
1382
|
+
file: this._path,
|
|
1383
|
+
bytesSearched: headerBytes,
|
|
1384
|
+
maxHeaderSize: MAX_HEADER_SIZE,
|
|
1385
|
+
stage: "readData"
|
|
1386
|
+
}
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1255
1389
|
}
|
|
1256
1390
|
} catch (error) {
|
|
1257
|
-
|
|
1391
|
+
const isExpectedEarlyClose = headerDelimiterIndex !== -1 && error !== null && typeof error === "object" && /** @type {{code?: unknown}} */
|
|
1392
|
+
error.code === "ERR_STREAM_PREMATURE_CLOSE";
|
|
1393
|
+
if (!isExpectedEarlyClose) {
|
|
1258
1394
|
throw error;
|
|
1259
1395
|
}
|
|
1260
1396
|
}
|
|
@@ -1267,6 +1403,7 @@ var init_QvdFileReader = __esm({
|
|
|
1267
1403
|
}
|
|
1268
1404
|
);
|
|
1269
1405
|
}
|
|
1406
|
+
headerBuffer = Buffer.concat(headerChunks);
|
|
1270
1407
|
const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
|
|
1271
1408
|
const headerXml = headerBuffer.subarray(0, headerEndIndex).toString();
|
|
1272
1409
|
const headerObj = await xml2__default.default.parseStringPromise(headerXml, { explicitArray: false });
|
|
@@ -1283,12 +1420,50 @@ var init_QvdFileReader = __esm({
|
|
|
1283
1420
|
const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1284
1421
|
const rowsToLoad = Math.min(maxRows, totalRows);
|
|
1285
1422
|
validateSymbolTableSizeEarly(symbolTableLength, this._path);
|
|
1423
|
+
for (const [name, value] of [
|
|
1424
|
+
["Offset", symbolTableLength],
|
|
1425
|
+
["RecordByteSize", recordSize],
|
|
1426
|
+
["NoOfRecords", totalRows]
|
|
1427
|
+
]) {
|
|
1428
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
|
1429
|
+
throw new exports.QvdCorruptedError(`Invalid header value: ${name}`, {
|
|
1430
|
+
name,
|
|
1431
|
+
value,
|
|
1432
|
+
file: this._path,
|
|
1433
|
+
stage: "readData"
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1286
1437
|
const indexTableBytesToRead = rowsToLoad * recordSize;
|
|
1287
1438
|
const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
|
|
1288
|
-
const fd = await
|
|
1439
|
+
const fd = await fs__default.default.promises.open(this._path, "r");
|
|
1289
1440
|
try {
|
|
1290
|
-
|
|
1291
|
-
|
|
1441
|
+
const { size: fileSize } = await fd.stat();
|
|
1442
|
+
this._fileSize = fileSize;
|
|
1443
|
+
if (totalBytesToRead > fileSize) {
|
|
1444
|
+
throw new exports.QvdCorruptedError("The file is shorter than its header claims.", {
|
|
1445
|
+
file: this._path,
|
|
1446
|
+
fileSize,
|
|
1447
|
+
requiredBytes: totalBytesToRead,
|
|
1448
|
+
stage: "readData"
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
this._buffer = Buffer.alloc(totalBytesToRead);
|
|
1452
|
+
let position = 0;
|
|
1453
|
+
while (position < totalBytesToRead) {
|
|
1454
|
+
const length = Math.min(READ_CHUNK_SIZE, totalBytesToRead - position);
|
|
1455
|
+
const { bytesRead } = await fd.read(this._buffer, position, length, position);
|
|
1456
|
+
if (bytesRead === 0) {
|
|
1457
|
+
throw new exports.QvdCorruptedError("Unexpected end of file while reading QVD data.", {
|
|
1458
|
+
file: this._path,
|
|
1459
|
+
fileSize,
|
|
1460
|
+
bytesRead: position,
|
|
1461
|
+
requiredBytes: totalBytesToRead,
|
|
1462
|
+
stage: "readData"
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
position += bytesRead;
|
|
1466
|
+
}
|
|
1292
1467
|
} finally {
|
|
1293
1468
|
await fd.close();
|
|
1294
1469
|
}
|
|
@@ -1390,7 +1565,10 @@ var init_QvdFileReader = __esm({
|
|
|
1390
1565
|
} else {
|
|
1391
1566
|
symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
|
|
1392
1567
|
}
|
|
1393
|
-
symbolIndex
|
|
1568
|
+
symbolIndex += bias;
|
|
1569
|
+
if (symbolIndex < 0) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1394
1572
|
const fieldName = field["FieldName"];
|
|
1395
1573
|
symbolUsage.get(fieldName)?.add(symbolIndex);
|
|
1396
1574
|
});
|
|
@@ -1495,7 +1673,8 @@ var init_QvdFileReader = __esm({
|
|
|
1495
1673
|
this._indexTableOffset,
|
|
1496
1674
|
this._buffer.length,
|
|
1497
1675
|
rowsToLoad,
|
|
1498
|
-
this._path
|
|
1676
|
+
this._path,
|
|
1677
|
+
this._fileSize
|
|
1499
1678
|
);
|
|
1500
1679
|
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1501
1680
|
for (const field of fields) {
|
|
@@ -1531,14 +1710,33 @@ var init_QvdFileReader = __esm({
|
|
|
1531
1710
|
});
|
|
1532
1711
|
this._indexTable.push(symbolIndices);
|
|
1533
1712
|
}
|
|
1713
|
+
if (this._indexTable.length !== rowsToLoad) {
|
|
1714
|
+
throw new exports.QvdCorruptedError("Index table contains fewer records than expected", {
|
|
1715
|
+
expectedRows: rowsToLoad,
|
|
1716
|
+
actualRows: this._indexTable.length,
|
|
1717
|
+
totalRows,
|
|
1718
|
+
recordSize,
|
|
1719
|
+
file: this._path,
|
|
1720
|
+
stage: "parseIndexTable"
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1534
1723
|
}
|
|
1535
1724
|
/**
|
|
1536
1725
|
* Loads the QVD file into memory and parses it.
|
|
1537
1726
|
*
|
|
1538
1727
|
* @param {number|null} maxRows The maximum number of rows to load. If null, all rows are loaded.
|
|
1728
|
+
* Must be a non-negative integer when given.
|
|
1729
|
+
* @throws {QvdValidationError} If maxRows is neither null nor a non-negative integer.
|
|
1539
1730
|
* @return {Promise<QvdDataFrame>} The loaded QVD file.
|
|
1540
1731
|
*/
|
|
1541
1732
|
async load(maxRows = null) {
|
|
1733
|
+
if (maxRows !== null && (typeof maxRows !== "number" || !Number.isInteger(maxRows) || maxRows < 0)) {
|
|
1734
|
+
throw new exports.QvdValidationError("maxRows must be a non-negative integer, or null to load all rows", {
|
|
1735
|
+
provided: maxRows,
|
|
1736
|
+
type: typeof maxRows,
|
|
1737
|
+
file: this._path
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1542
1740
|
await this._readData(maxRows);
|
|
1543
1741
|
await this._parseHeader();
|
|
1544
1742
|
let symbolsToKeep = null;
|
|
@@ -1573,7 +1771,7 @@ var init_QvdFileReader = __esm({
|
|
|
1573
1771
|
const symbol = this._symbolTable?.[fieldIndex]?.[symbolIndex];
|
|
1574
1772
|
const value = symbol?.toPrimaryValue();
|
|
1575
1773
|
if (typeof value === "string") {
|
|
1576
|
-
if (!isNaN(Number(value))) {
|
|
1774
|
+
if (value.trim() !== "" && !isNaN(Number(value))) {
|
|
1577
1775
|
return Number(value);
|
|
1578
1776
|
}
|
|
1579
1777
|
}
|
|
@@ -1983,7 +2181,11 @@ var init_QvdDataFrame = __esm({
|
|
|
1983
2181
|
*
|
|
1984
2182
|
* @param {string} path The path to the QVD file.
|
|
1985
2183
|
* @param {Object} [options] Optional writing options.
|
|
1986
|
-
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2184
|
+
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2185
|
+
* must be within this directory, with symlinks resolved first, so a link inside it that points
|
|
2186
|
+
* outside it is rejected. Defaults to the current working directory. To permit an entire
|
|
2187
|
+
* volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
|
|
2188
|
+
* back to the working directory rather than removing the restriction.
|
|
1987
2189
|
* @param {Function} [options.onProgress] Optional progress callback function that receives progress updates during write operations.
|
|
1988
2190
|
*/
|
|
1989
2191
|
async toQvd(path3, options = {}) {
|
|
@@ -1999,11 +2201,17 @@ var init_QvdDataFrame = __esm({
|
|
|
1999
2201
|
*
|
|
2000
2202
|
* @param {string} path The path to the QVD file.
|
|
2001
2203
|
* @param {Object} [options] Optional loading options.
|
|
2002
|
-
* @param {number|null} [options.maxRows] The maximum number of rows to load.
|
|
2003
|
-
*
|
|
2204
|
+
* @param {number|null} [options.maxRows] The maximum number of rows to load. Must be a non-negative
|
|
2205
|
+
* integer; if not specified or null, all rows are loaded. Anything else throws a QvdValidationError.
|
|
2206
|
+
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2207
|
+
* must be within this directory, with symlinks resolved first, so a link inside it that points
|
|
2208
|
+
* outside it is rejected. Defaults to the current working directory. To permit an entire
|
|
2209
|
+
* volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
|
|
2210
|
+
* back to the working directory rather than removing the restriction.
|
|
2004
2211
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage
|
|
2005
2212
|
* of available memory (or V8 heap limit, whichever is smaller) can be used. Default is 0.3 (30%).
|
|
2006
2213
|
* Increase this (e.g., to 0.5 or 0.7) when running with larger heap sizes via --max-old-space-size.
|
|
2214
|
+
* @throws {QvdValidationError} If options.maxRows is neither null/undefined nor a non-negative integer.
|
|
2007
2215
|
* @return {Promise<QvdDataFrame>} The data frame of the QVD file.
|
|
2008
2216
|
*/
|
|
2009
2217
|
static async fromQvd(path3, options = {}) {
|