qvdjs 0.9.0 → 0.9.1
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 +191 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +190 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var path = require('path');
|
|
4
4
|
var fs2 = require('fs');
|
|
5
5
|
var crypto = require('crypto');
|
|
6
6
|
var xml2 = require('xml2js');
|
|
@@ -10,7 +10,7 @@ var v8 = require('v8');
|
|
|
10
10
|
|
|
11
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
12
|
|
|
13
|
-
var
|
|
13
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
14
14
|
var fs2__default = /*#__PURE__*/_interopDefault(fs2);
|
|
15
15
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
16
16
|
var xml2__default = /*#__PURE__*/_interopDefault(xml2);
|
|
@@ -262,20 +262,42 @@ var init_QvdSymbol = __esm({
|
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
});
|
|
265
|
+
function isWithinDirectory(resolvedBaseDir, resolvedPath) {
|
|
266
|
+
const isCaseInsensitiveFS = process.platform === "win32" || process.platform === "darwin";
|
|
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
|
+
}
|
|
265
278
|
function validatePath(filePath, allowedDir) {
|
|
279
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
280
|
+
throw new exports.QvdValidationError("filePath must be a non-empty string", {
|
|
281
|
+
provided: filePath,
|
|
282
|
+
type: typeof filePath
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (allowedDir !== void 0 && allowedDir !== null && typeof allowedDir !== "string") {
|
|
286
|
+
throw new exports.QvdValidationError("allowedDir must be a string, null or undefined", {
|
|
287
|
+
provided: allowedDir,
|
|
288
|
+
type: typeof allowedDir
|
|
289
|
+
});
|
|
290
|
+
}
|
|
266
291
|
if (filePath.includes("\0")) {
|
|
267
292
|
throw new exports.QvdSecurityError("Path traversal detected: Null byte in path", {
|
|
268
293
|
path: filePath,
|
|
269
294
|
reason: "null_byte"
|
|
270
295
|
});
|
|
271
296
|
}
|
|
297
|
+
const resolvedPath = path__default.default.resolve(filePath);
|
|
272
298
|
const baseDir = allowedDir || process.cwd();
|
|
273
|
-
const resolvedBaseDir =
|
|
274
|
-
|
|
275
|
-
const isCaseInsensitiveFS = process.platform === "win32" || process.platform === "darwin";
|
|
276
|
-
const baseForComparison = isCaseInsensitiveFS ? resolvedBaseDir.toLowerCase() : resolvedBaseDir;
|
|
277
|
-
const pathForComparison = isCaseInsensitiveFS ? resolvedPath.toLowerCase() : resolvedPath;
|
|
278
|
-
if (!pathForComparison.startsWith(baseForComparison + path2__default.default.sep) && pathForComparison !== baseForComparison) {
|
|
299
|
+
const resolvedBaseDir = path__default.default.resolve(baseDir);
|
|
300
|
+
if (!isWithinDirectory(resolvedBaseDir, resolvedPath)) {
|
|
279
301
|
throw new exports.QvdSecurityError("Path traversal detected: Access denied", {
|
|
280
302
|
path: filePath,
|
|
281
303
|
resolvedPath,
|
|
@@ -309,7 +331,9 @@ var init_QvdFileWriter = __esm({
|
|
|
309
331
|
* @param {QvdDataFrame} df The data frame to write to the QVD file.
|
|
310
332
|
* @param {Object} [options={}] Options for the writer.
|
|
311
333
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
312
|
-
* path must be within this directory. Defaults to current working directory.
|
|
334
|
+
* path must be within this directory. Defaults to the current working directory. To permit
|
|
335
|
+
* an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
|
|
336
|
+
* empty value falls back to the working directory rather than removing the restriction.
|
|
313
337
|
* @param {Function} [options.onProgress] Optional progress callback function.
|
|
314
338
|
*/
|
|
315
339
|
constructor(filePath, df, options = {}) {
|
|
@@ -381,7 +405,7 @@ var init_QvdFileWriter = __esm({
|
|
|
381
405
|
SourceFileUtcTime: existingMetadata.SourceFileUtcTime || "",
|
|
382
406
|
SourceFileSize: existingMetadata.SourceFileSize || -1,
|
|
383
407
|
StaleUtcTime: existingMetadata.StaleUtcTime || "",
|
|
384
|
-
TableName: existingMetadata.TableName ||
|
|
408
|
+
TableName: existingMetadata.TableName || path__default.default.basename(this._path, path__default.default.extname(this._path)),
|
|
385
409
|
Compression: existingMetadata.Compression || "",
|
|
386
410
|
Comment: existingMetadata.Comment || "",
|
|
387
411
|
EncryptionInfo: existingMetadata.EncryptionInfo || "",
|
|
@@ -401,7 +425,7 @@ var init_QvdFileWriter = __esm({
|
|
|
401
425
|
SourceFileUtcTime: "",
|
|
402
426
|
SourceFileSize: -1,
|
|
403
427
|
StaleUtcTime: "",
|
|
404
|
-
TableName:
|
|
428
|
+
TableName: path__default.default.basename(this._path, path__default.default.extname(this._path)),
|
|
405
429
|
Compression: "",
|
|
406
430
|
Comment: "",
|
|
407
431
|
EncryptionInfo: "",
|
|
@@ -571,21 +595,22 @@ var init_QvdFileWriter = __esm({
|
|
|
571
595
|
this._emitProgress("index-table", processedRows, numRows);
|
|
572
596
|
}
|
|
573
597
|
});
|
|
574
|
-
this._df.columns.forEach((column) => {
|
|
575
|
-
const bitOffset = this._indexTableMetadata?.slice(0,
|
|
598
|
+
this._df.columns.forEach((column, columnIndex) => {
|
|
599
|
+
const bitOffset = this._indexTableMetadata?.slice(0, columnIndex).reduce((sum, metadata) => sum + metadata[1], 0);
|
|
576
600
|
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];
|
|
601
|
+
const fieldContainsNull = this._symbolTableMetadata?.[columnIndex][2];
|
|
583
602
|
const bias = fieldContainsNull ? -2 : 0;
|
|
603
|
+
const symbolCount = this._symbolTable?.[columnIndex].length ?? 0;
|
|
604
|
+
const maxStoredIndex = symbolCount === 0 ? 0 : symbolCount - 1 + (fieldContainsNull ? 2 : 0);
|
|
605
|
+
const bitWidth = maxStoredIndex === 0 ? 0 : 32 - Math.clz32(maxStoredIndex);
|
|
584
606
|
this._indexTableMetadata?.push([bitOffset, bitWidth, bias]);
|
|
585
607
|
this._indexTable.forEach((indices) => {
|
|
586
|
-
const bitString = indices[
|
|
587
|
-
|
|
588
|
-
|
|
608
|
+
const bitString = indices[columnIndex];
|
|
609
|
+
assert__default.default(
|
|
610
|
+
bitString === "0" || bitString.length <= bitWidth,
|
|
611
|
+
`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.`
|
|
612
|
+
);
|
|
613
|
+
indices[columnIndex] = bitWidth === 0 ? "" : bitString.padStart(bitWidth, "0");
|
|
589
614
|
});
|
|
590
615
|
});
|
|
591
616
|
this._indexBuffer = Buffer.concat(
|
|
@@ -593,6 +618,9 @@ var init_QvdFileWriter = __esm({
|
|
|
593
618
|
this._indexTable.map((indices) => {
|
|
594
619
|
indices.reverse();
|
|
595
620
|
const bits = indices.join("");
|
|
621
|
+
if (bits.length === 0) {
|
|
622
|
+
return Buffer.from(Uint8Array.from([0]));
|
|
623
|
+
}
|
|
596
624
|
const paddingWidth = (8 - bits.length % 8) % 8;
|
|
597
625
|
const paddedBits = bits.padStart(bits.length + paddingWidth, "0");
|
|
598
626
|
const bytes = paddedBits.match(/.{1,8}/g)?.map((byte) => parseInt(byte, 2));
|
|
@@ -805,7 +833,7 @@ function validateFieldMetadata(field, symbolBufferLength, filePath) {
|
|
|
805
833
|
});
|
|
806
834
|
}
|
|
807
835
|
}
|
|
808
|
-
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath) {
|
|
836
|
+
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath, fileSize = null) {
|
|
809
837
|
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
810
838
|
throw new exports.QvdCorruptedError("Invalid record byte size", {
|
|
811
839
|
recordSize,
|
|
@@ -851,16 +879,40 @@ function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, ind
|
|
|
851
879
|
stage: "parseIndexTable"
|
|
852
880
|
});
|
|
853
881
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
throw new exports.QvdCorruptedError("Index table length unreasonably large", {
|
|
882
|
+
if (indexTableLength !== totalRows * recordSize) {
|
|
883
|
+
throw new exports.QvdCorruptedError("Index table length inconsistent with record count", {
|
|
857
884
|
indexTableLength,
|
|
858
|
-
|
|
885
|
+
expectedLength: totalRows * recordSize,
|
|
886
|
+
totalRows,
|
|
887
|
+
recordSize,
|
|
859
888
|
file: filePath,
|
|
860
889
|
stage: "parseIndexTable"
|
|
861
890
|
});
|
|
862
891
|
}
|
|
892
|
+
if (fileSize !== null) {
|
|
893
|
+
if (indexTableOffset + indexTableLength > fileSize) {
|
|
894
|
+
throw new exports.QvdCorruptedError("Index table extends beyond the end of the file", {
|
|
895
|
+
indexTableOffset,
|
|
896
|
+
indexTableLength,
|
|
897
|
+
fileSize,
|
|
898
|
+
file: filePath,
|
|
899
|
+
stage: "parseIndexTable"
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
863
903
|
const requiredIndexBytes = rowsToLoad * recordSize;
|
|
904
|
+
if (indexTableOffset + requiredIndexBytes > bufferLength) {
|
|
905
|
+
throw new exports.QvdCorruptedError("Index table truncated", {
|
|
906
|
+
indexTableOffset,
|
|
907
|
+
requiredBytes: requiredIndexBytes,
|
|
908
|
+
availableBytes: Math.max(0, bufferLength - indexTableOffset),
|
|
909
|
+
rowsToLoad,
|
|
910
|
+
recordSize,
|
|
911
|
+
bufferSize: bufferLength,
|
|
912
|
+
file: filePath,
|
|
913
|
+
stage: "parseIndexTable"
|
|
914
|
+
});
|
|
915
|
+
}
|
|
864
916
|
if (indexTableLength < requiredIndexBytes) {
|
|
865
917
|
throw new exports.QvdCorruptedError("Index table length smaller than required", {
|
|
866
918
|
indexTableLength,
|
|
@@ -1171,7 +1223,7 @@ var QvdFileReader_exports = {};
|
|
|
1171
1223
|
__export(QvdFileReader_exports, {
|
|
1172
1224
|
QvdFileReader: () => exports.QvdFileReader
|
|
1173
1225
|
});
|
|
1174
|
-
exports.QvdFileReader = void 0;
|
|
1226
|
+
var MAX_HEADER_SIZE, READ_CHUNK_SIZE; exports.QvdFileReader = void 0;
|
|
1175
1227
|
var init_QvdFileReader = __esm({
|
|
1176
1228
|
"src/QvdFileReader.js"() {
|
|
1177
1229
|
init_QvdDataFrame();
|
|
@@ -1181,6 +1233,8 @@ var init_QvdFileReader = __esm({
|
|
|
1181
1233
|
init_memoryUtils();
|
|
1182
1234
|
init_validationUtils();
|
|
1183
1235
|
init_symbolParser();
|
|
1236
|
+
MAX_HEADER_SIZE = 16 * 1024 * 1024;
|
|
1237
|
+
READ_CHUNK_SIZE = 512 * 1024 * 1024;
|
|
1184
1238
|
exports.QvdFileReader = class {
|
|
1185
1239
|
/**
|
|
1186
1240
|
* Constructs a new QVD file parser.
|
|
@@ -1188,7 +1242,9 @@ var init_QvdFileReader = __esm({
|
|
|
1188
1242
|
* @param {string} filePath The path to the QVD file to load.
|
|
1189
1243
|
* @param {Object} [options={}] Options for the reader.
|
|
1190
1244
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
1191
|
-
* path must be within this directory. Defaults to current working directory.
|
|
1245
|
+
* path must be within this directory. Defaults to the current working directory. To permit
|
|
1246
|
+
* an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
|
|
1247
|
+
* empty value falls back to the working directory rather than removing the restriction.
|
|
1192
1248
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
|
|
1193
1249
|
* what percentage of available memory (or V8 heap limit, whichever is smaller) can be used
|
|
1194
1250
|
* for loading QVD files. Default is 0.3 (30%). Increase for larger heap configurations.
|
|
@@ -1204,6 +1260,7 @@ var init_QvdFileReader = __esm({
|
|
|
1204
1260
|
this._header = null;
|
|
1205
1261
|
this._symbolTable = null;
|
|
1206
1262
|
this._indexTable = null;
|
|
1263
|
+
this._fileSize = null;
|
|
1207
1264
|
}
|
|
1208
1265
|
/**
|
|
1209
1266
|
* Reads the binary data of the QVD file.
|
|
@@ -1235,6 +1292,7 @@ var init_QvdFileReader = __esm({
|
|
|
1235
1292
|
async _readData(maxRows = null) {
|
|
1236
1293
|
if (maxRows === null) {
|
|
1237
1294
|
this._buffer = await fs2__default.default.promises.readFile(this._path);
|
|
1295
|
+
this._fileSize = this._buffer.length;
|
|
1238
1296
|
return;
|
|
1239
1297
|
}
|
|
1240
1298
|
const HEADER_DELIMITER = "\r\n\0";
|
|
@@ -1242,19 +1300,41 @@ var init_QvdFileReader = __esm({
|
|
|
1242
1300
|
const stream = fs2__default.default.createReadStream(this._path, {
|
|
1243
1301
|
highWaterMark: CHUNK_SIZE
|
|
1244
1302
|
});
|
|
1303
|
+
const headerChunks = [];
|
|
1304
|
+
let headerBytes = 0;
|
|
1305
|
+
let tail = Buffer.alloc(0);
|
|
1245
1306
|
let headerBuffer = Buffer.alloc(0);
|
|
1246
1307
|
let headerDelimiterIndex = -1;
|
|
1247
1308
|
try {
|
|
1248
1309
|
for await (const chunk of stream) {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1310
|
+
const chunkStart = headerBytes;
|
|
1311
|
+
const searchBuffer = tail.length > 0 ? Buffer.concat([tail, chunk]) : chunk;
|
|
1312
|
+
const foundInSearch = searchBuffer.indexOf(HEADER_DELIMITER);
|
|
1313
|
+
headerChunks.push(chunk);
|
|
1314
|
+
headerBytes += chunk.length;
|
|
1315
|
+
if (foundInSearch !== -1) {
|
|
1316
|
+
headerDelimiterIndex = chunkStart - tail.length + foundInSearch;
|
|
1252
1317
|
stream.destroy();
|
|
1253
1318
|
break;
|
|
1254
1319
|
}
|
|
1320
|
+
tail = Buffer.from(searchBuffer.subarray(-(HEADER_DELIMITER.length - 1)));
|
|
1321
|
+
if (headerBytes > MAX_HEADER_SIZE) {
|
|
1322
|
+
stream.destroy();
|
|
1323
|
+
throw new exports.QvdCorruptedError(
|
|
1324
|
+
`The XML header delimiter was not found within the first ${MAX_HEADER_SIZE / (1024 * 1024)}MB of the file.`,
|
|
1325
|
+
{
|
|
1326
|
+
file: this._path,
|
|
1327
|
+
bytesSearched: headerBytes,
|
|
1328
|
+
maxHeaderSize: MAX_HEADER_SIZE,
|
|
1329
|
+
stage: "readData"
|
|
1330
|
+
}
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1255
1333
|
}
|
|
1256
1334
|
} catch (error) {
|
|
1257
|
-
|
|
1335
|
+
const isExpectedEarlyClose = headerDelimiterIndex !== -1 && error !== null && typeof error === "object" && /** @type {{code?: unknown}} */
|
|
1336
|
+
error.code === "ERR_STREAM_PREMATURE_CLOSE";
|
|
1337
|
+
if (!isExpectedEarlyClose) {
|
|
1258
1338
|
throw error;
|
|
1259
1339
|
}
|
|
1260
1340
|
}
|
|
@@ -1267,6 +1347,7 @@ var init_QvdFileReader = __esm({
|
|
|
1267
1347
|
}
|
|
1268
1348
|
);
|
|
1269
1349
|
}
|
|
1350
|
+
headerBuffer = Buffer.concat(headerChunks);
|
|
1270
1351
|
const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
|
|
1271
1352
|
const headerXml = headerBuffer.subarray(0, headerEndIndex).toString();
|
|
1272
1353
|
const headerObj = await xml2__default.default.parseStringPromise(headerXml, { explicitArray: false });
|
|
@@ -1283,12 +1364,50 @@ var init_QvdFileReader = __esm({
|
|
|
1283
1364
|
const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1284
1365
|
const rowsToLoad = Math.min(maxRows, totalRows);
|
|
1285
1366
|
validateSymbolTableSizeEarly(symbolTableLength, this._path);
|
|
1367
|
+
for (const [name, value] of [
|
|
1368
|
+
["Offset", symbolTableLength],
|
|
1369
|
+
["RecordByteSize", recordSize],
|
|
1370
|
+
["NoOfRecords", totalRows]
|
|
1371
|
+
]) {
|
|
1372
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
|
1373
|
+
throw new exports.QvdCorruptedError(`Invalid header value: ${name}`, {
|
|
1374
|
+
name,
|
|
1375
|
+
value,
|
|
1376
|
+
file: this._path,
|
|
1377
|
+
stage: "readData"
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1286
1381
|
const indexTableBytesToRead = rowsToLoad * recordSize;
|
|
1287
1382
|
const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
|
|
1288
1383
|
const fd = await fs2__default.default.promises.open(this._path, "r");
|
|
1289
1384
|
try {
|
|
1290
|
-
|
|
1291
|
-
|
|
1385
|
+
const { size: fileSize } = await fd.stat();
|
|
1386
|
+
this._fileSize = fileSize;
|
|
1387
|
+
if (totalBytesToRead > fileSize) {
|
|
1388
|
+
throw new exports.QvdCorruptedError("The file is shorter than its header claims.", {
|
|
1389
|
+
file: this._path,
|
|
1390
|
+
fileSize,
|
|
1391
|
+
requiredBytes: totalBytesToRead,
|
|
1392
|
+
stage: "readData"
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
this._buffer = Buffer.alloc(totalBytesToRead);
|
|
1396
|
+
let position = 0;
|
|
1397
|
+
while (position < totalBytesToRead) {
|
|
1398
|
+
const length = Math.min(READ_CHUNK_SIZE, totalBytesToRead - position);
|
|
1399
|
+
const { bytesRead } = await fd.read(this._buffer, position, length, position);
|
|
1400
|
+
if (bytesRead === 0) {
|
|
1401
|
+
throw new exports.QvdCorruptedError("Unexpected end of file while reading QVD data.", {
|
|
1402
|
+
file: this._path,
|
|
1403
|
+
fileSize,
|
|
1404
|
+
bytesRead: position,
|
|
1405
|
+
requiredBytes: totalBytesToRead,
|
|
1406
|
+
stage: "readData"
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
position += bytesRead;
|
|
1410
|
+
}
|
|
1292
1411
|
} finally {
|
|
1293
1412
|
await fd.close();
|
|
1294
1413
|
}
|
|
@@ -1390,7 +1509,10 @@ var init_QvdFileReader = __esm({
|
|
|
1390
1509
|
} else {
|
|
1391
1510
|
symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
|
|
1392
1511
|
}
|
|
1393
|
-
symbolIndex
|
|
1512
|
+
symbolIndex += bias;
|
|
1513
|
+
if (symbolIndex < 0) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1394
1516
|
const fieldName = field["FieldName"];
|
|
1395
1517
|
symbolUsage.get(fieldName)?.add(symbolIndex);
|
|
1396
1518
|
});
|
|
@@ -1495,7 +1617,8 @@ var init_QvdFileReader = __esm({
|
|
|
1495
1617
|
this._indexTableOffset,
|
|
1496
1618
|
this._buffer.length,
|
|
1497
1619
|
rowsToLoad,
|
|
1498
|
-
this._path
|
|
1620
|
+
this._path,
|
|
1621
|
+
this._fileSize
|
|
1499
1622
|
);
|
|
1500
1623
|
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1501
1624
|
for (const field of fields) {
|
|
@@ -1531,14 +1654,33 @@ var init_QvdFileReader = __esm({
|
|
|
1531
1654
|
});
|
|
1532
1655
|
this._indexTable.push(symbolIndices);
|
|
1533
1656
|
}
|
|
1657
|
+
if (this._indexTable.length !== rowsToLoad) {
|
|
1658
|
+
throw new exports.QvdCorruptedError("Index table contains fewer records than expected", {
|
|
1659
|
+
expectedRows: rowsToLoad,
|
|
1660
|
+
actualRows: this._indexTable.length,
|
|
1661
|
+
totalRows,
|
|
1662
|
+
recordSize,
|
|
1663
|
+
file: this._path,
|
|
1664
|
+
stage: "parseIndexTable"
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1534
1667
|
}
|
|
1535
1668
|
/**
|
|
1536
1669
|
* Loads the QVD file into memory and parses it.
|
|
1537
1670
|
*
|
|
1538
1671
|
* @param {number|null} maxRows The maximum number of rows to load. If null, all rows are loaded.
|
|
1672
|
+
* Must be a non-negative integer when given.
|
|
1673
|
+
* @throws {QvdValidationError} If maxRows is neither null nor a non-negative integer.
|
|
1539
1674
|
* @return {Promise<QvdDataFrame>} The loaded QVD file.
|
|
1540
1675
|
*/
|
|
1541
1676
|
async load(maxRows = null) {
|
|
1677
|
+
if (maxRows !== null && (typeof maxRows !== "number" || !Number.isInteger(maxRows) || maxRows < 0)) {
|
|
1678
|
+
throw new exports.QvdValidationError("maxRows must be a non-negative integer, or null to load all rows", {
|
|
1679
|
+
provided: maxRows,
|
|
1680
|
+
type: typeof maxRows,
|
|
1681
|
+
file: this._path
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1542
1684
|
await this._readData(maxRows);
|
|
1543
1685
|
await this._parseHeader();
|
|
1544
1686
|
let symbolsToKeep = null;
|
|
@@ -1573,7 +1715,7 @@ var init_QvdFileReader = __esm({
|
|
|
1573
1715
|
const symbol = this._symbolTable?.[fieldIndex]?.[symbolIndex];
|
|
1574
1716
|
const value = symbol?.toPrimaryValue();
|
|
1575
1717
|
if (typeof value === "string") {
|
|
1576
|
-
if (!isNaN(Number(value))) {
|
|
1718
|
+
if (value.trim() !== "" && !isNaN(Number(value))) {
|
|
1577
1719
|
return Number(value);
|
|
1578
1720
|
}
|
|
1579
1721
|
}
|
|
@@ -1983,7 +2125,10 @@ var init_QvdDataFrame = __esm({
|
|
|
1983
2125
|
*
|
|
1984
2126
|
* @param {string} path The path to the QVD file.
|
|
1985
2127
|
* @param {Object} [options] Optional writing options.
|
|
1986
|
-
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2128
|
+
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2129
|
+
* must be within this directory. Defaults to the current working directory. To permit an entire
|
|
2130
|
+
* volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
|
|
2131
|
+
* back to the working directory rather than removing the restriction.
|
|
1987
2132
|
* @param {Function} [options.onProgress] Optional progress callback function that receives progress updates during write operations.
|
|
1988
2133
|
*/
|
|
1989
2134
|
async toQvd(path3, options = {}) {
|
|
@@ -1999,11 +2144,16 @@ var init_QvdDataFrame = __esm({
|
|
|
1999
2144
|
*
|
|
2000
2145
|
* @param {string} path The path to the QVD file.
|
|
2001
2146
|
* @param {Object} [options] Optional loading options.
|
|
2002
|
-
* @param {number|null} [options.maxRows] The maximum number of rows to load.
|
|
2003
|
-
*
|
|
2147
|
+
* @param {number|null} [options.maxRows] The maximum number of rows to load. Must be a non-negative
|
|
2148
|
+
* integer; if not specified or null, all rows are loaded. Anything else throws a QvdValidationError.
|
|
2149
|
+
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2150
|
+
* must be within this directory. Defaults to the current working directory. To permit an entire
|
|
2151
|
+
* volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
|
|
2152
|
+
* back to the working directory rather than removing the restriction.
|
|
2004
2153
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage
|
|
2005
2154
|
* of available memory (or V8 heap limit, whichever is smaller) can be used. Default is 0.3 (30%).
|
|
2006
2155
|
* Increase this (e.g., to 0.5 or 0.7) when running with larger heap sizes via --max-old-space-size.
|
|
2156
|
+
* @throws {QvdValidationError} If options.maxRows is neither null/undefined nor a non-negative integer.
|
|
2007
2157
|
* @return {Promise<QvdDataFrame>} The data frame of the QVD file.
|
|
2008
2158
|
*/
|
|
2009
2159
|
static async fromQvd(path3, options = {}) {
|