qvdjs 0.8.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 +194 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +193 -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));
|
|
@@ -676,6 +704,9 @@ function estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
|
|
|
676
704
|
return keptSymbolsMemory + skippedSymbolsMemory;
|
|
677
705
|
}
|
|
678
706
|
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.3) {
|
|
707
|
+
if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
|
|
708
|
+
throw new exports.QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", { safetyFactor });
|
|
709
|
+
}
|
|
679
710
|
const availableMemory = os__default.default.freemem();
|
|
680
711
|
const heapLimit = getHeapLimit();
|
|
681
712
|
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
@@ -802,7 +833,7 @@ function validateFieldMetadata(field, symbolBufferLength, filePath) {
|
|
|
802
833
|
});
|
|
803
834
|
}
|
|
804
835
|
}
|
|
805
|
-
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath) {
|
|
836
|
+
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath, fileSize = null) {
|
|
806
837
|
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
807
838
|
throw new exports.QvdCorruptedError("Invalid record byte size", {
|
|
808
839
|
recordSize,
|
|
@@ -848,16 +879,40 @@ function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, ind
|
|
|
848
879
|
stage: "parseIndexTable"
|
|
849
880
|
});
|
|
850
881
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
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", {
|
|
854
884
|
indexTableLength,
|
|
855
|
-
|
|
885
|
+
expectedLength: totalRows * recordSize,
|
|
886
|
+
totalRows,
|
|
887
|
+
recordSize,
|
|
856
888
|
file: filePath,
|
|
857
889
|
stage: "parseIndexTable"
|
|
858
890
|
});
|
|
859
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
|
+
}
|
|
860
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
|
+
}
|
|
861
916
|
if (indexTableLength < requiredIndexBytes) {
|
|
862
917
|
throw new exports.QvdCorruptedError("Index table length smaller than required", {
|
|
863
918
|
indexTableLength,
|
|
@@ -1168,7 +1223,7 @@ var QvdFileReader_exports = {};
|
|
|
1168
1223
|
__export(QvdFileReader_exports, {
|
|
1169
1224
|
QvdFileReader: () => exports.QvdFileReader
|
|
1170
1225
|
});
|
|
1171
|
-
exports.QvdFileReader = void 0;
|
|
1226
|
+
var MAX_HEADER_SIZE, READ_CHUNK_SIZE; exports.QvdFileReader = void 0;
|
|
1172
1227
|
var init_QvdFileReader = __esm({
|
|
1173
1228
|
"src/QvdFileReader.js"() {
|
|
1174
1229
|
init_QvdDataFrame();
|
|
@@ -1178,6 +1233,8 @@ var init_QvdFileReader = __esm({
|
|
|
1178
1233
|
init_memoryUtils();
|
|
1179
1234
|
init_validationUtils();
|
|
1180
1235
|
init_symbolParser();
|
|
1236
|
+
MAX_HEADER_SIZE = 16 * 1024 * 1024;
|
|
1237
|
+
READ_CHUNK_SIZE = 512 * 1024 * 1024;
|
|
1181
1238
|
exports.QvdFileReader = class {
|
|
1182
1239
|
/**
|
|
1183
1240
|
* Constructs a new QVD file parser.
|
|
@@ -1185,7 +1242,9 @@ var init_QvdFileReader = __esm({
|
|
|
1185
1242
|
* @param {string} filePath The path to the QVD file to load.
|
|
1186
1243
|
* @param {Object} [options={}] Options for the reader.
|
|
1187
1244
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
1188
|
-
* 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.
|
|
1189
1248
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
|
|
1190
1249
|
* what percentage of available memory (or V8 heap limit, whichever is smaller) can be used
|
|
1191
1250
|
* for loading QVD files. Default is 0.3 (30%). Increase for larger heap configurations.
|
|
@@ -1201,6 +1260,7 @@ var init_QvdFileReader = __esm({
|
|
|
1201
1260
|
this._header = null;
|
|
1202
1261
|
this._symbolTable = null;
|
|
1203
1262
|
this._indexTable = null;
|
|
1263
|
+
this._fileSize = null;
|
|
1204
1264
|
}
|
|
1205
1265
|
/**
|
|
1206
1266
|
* Reads the binary data of the QVD file.
|
|
@@ -1232,6 +1292,7 @@ var init_QvdFileReader = __esm({
|
|
|
1232
1292
|
async _readData(maxRows = null) {
|
|
1233
1293
|
if (maxRows === null) {
|
|
1234
1294
|
this._buffer = await fs2__default.default.promises.readFile(this._path);
|
|
1295
|
+
this._fileSize = this._buffer.length;
|
|
1235
1296
|
return;
|
|
1236
1297
|
}
|
|
1237
1298
|
const HEADER_DELIMITER = "\r\n\0";
|
|
@@ -1239,19 +1300,41 @@ var init_QvdFileReader = __esm({
|
|
|
1239
1300
|
const stream = fs2__default.default.createReadStream(this._path, {
|
|
1240
1301
|
highWaterMark: CHUNK_SIZE
|
|
1241
1302
|
});
|
|
1303
|
+
const headerChunks = [];
|
|
1304
|
+
let headerBytes = 0;
|
|
1305
|
+
let tail = Buffer.alloc(0);
|
|
1242
1306
|
let headerBuffer = Buffer.alloc(0);
|
|
1243
1307
|
let headerDelimiterIndex = -1;
|
|
1244
1308
|
try {
|
|
1245
1309
|
for await (const chunk of stream) {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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;
|
|
1249
1317
|
stream.destroy();
|
|
1250
1318
|
break;
|
|
1251
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
|
+
}
|
|
1252
1333
|
}
|
|
1253
1334
|
} catch (error) {
|
|
1254
|
-
|
|
1335
|
+
const isExpectedEarlyClose = headerDelimiterIndex !== -1 && error !== null && typeof error === "object" && /** @type {{code?: unknown}} */
|
|
1336
|
+
error.code === "ERR_STREAM_PREMATURE_CLOSE";
|
|
1337
|
+
if (!isExpectedEarlyClose) {
|
|
1255
1338
|
throw error;
|
|
1256
1339
|
}
|
|
1257
1340
|
}
|
|
@@ -1264,6 +1347,7 @@ var init_QvdFileReader = __esm({
|
|
|
1264
1347
|
}
|
|
1265
1348
|
);
|
|
1266
1349
|
}
|
|
1350
|
+
headerBuffer = Buffer.concat(headerChunks);
|
|
1267
1351
|
const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
|
|
1268
1352
|
const headerXml = headerBuffer.subarray(0, headerEndIndex).toString();
|
|
1269
1353
|
const headerObj = await xml2__default.default.parseStringPromise(headerXml, { explicitArray: false });
|
|
@@ -1280,12 +1364,50 @@ var init_QvdFileReader = __esm({
|
|
|
1280
1364
|
const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1281
1365
|
const rowsToLoad = Math.min(maxRows, totalRows);
|
|
1282
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
|
+
}
|
|
1283
1381
|
const indexTableBytesToRead = rowsToLoad * recordSize;
|
|
1284
1382
|
const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
|
|
1285
1383
|
const fd = await fs2__default.default.promises.open(this._path, "r");
|
|
1286
1384
|
try {
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
+
}
|
|
1289
1411
|
} finally {
|
|
1290
1412
|
await fd.close();
|
|
1291
1413
|
}
|
|
@@ -1387,7 +1509,10 @@ var init_QvdFileReader = __esm({
|
|
|
1387
1509
|
} else {
|
|
1388
1510
|
symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
|
|
1389
1511
|
}
|
|
1390
|
-
symbolIndex
|
|
1512
|
+
symbolIndex += bias;
|
|
1513
|
+
if (symbolIndex < 0) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1391
1516
|
const fieldName = field["FieldName"];
|
|
1392
1517
|
symbolUsage.get(fieldName)?.add(symbolIndex);
|
|
1393
1518
|
});
|
|
@@ -1492,7 +1617,8 @@ var init_QvdFileReader = __esm({
|
|
|
1492
1617
|
this._indexTableOffset,
|
|
1493
1618
|
this._buffer.length,
|
|
1494
1619
|
rowsToLoad,
|
|
1495
|
-
this._path
|
|
1620
|
+
this._path,
|
|
1621
|
+
this._fileSize
|
|
1496
1622
|
);
|
|
1497
1623
|
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1498
1624
|
for (const field of fields) {
|
|
@@ -1528,14 +1654,33 @@ var init_QvdFileReader = __esm({
|
|
|
1528
1654
|
});
|
|
1529
1655
|
this._indexTable.push(symbolIndices);
|
|
1530
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
|
+
}
|
|
1531
1667
|
}
|
|
1532
1668
|
/**
|
|
1533
1669
|
* Loads the QVD file into memory and parses it.
|
|
1534
1670
|
*
|
|
1535
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.
|
|
1536
1674
|
* @return {Promise<QvdDataFrame>} The loaded QVD file.
|
|
1537
1675
|
*/
|
|
1538
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
|
+
}
|
|
1539
1684
|
await this._readData(maxRows);
|
|
1540
1685
|
await this._parseHeader();
|
|
1541
1686
|
let symbolsToKeep = null;
|
|
@@ -1570,7 +1715,7 @@ var init_QvdFileReader = __esm({
|
|
|
1570
1715
|
const symbol = this._symbolTable?.[fieldIndex]?.[symbolIndex];
|
|
1571
1716
|
const value = symbol?.toPrimaryValue();
|
|
1572
1717
|
if (typeof value === "string") {
|
|
1573
|
-
if (!isNaN(Number(value))) {
|
|
1718
|
+
if (value.trim() !== "" && !isNaN(Number(value))) {
|
|
1574
1719
|
return Number(value);
|
|
1575
1720
|
}
|
|
1576
1721
|
}
|
|
@@ -1980,7 +2125,10 @@ var init_QvdDataFrame = __esm({
|
|
|
1980
2125
|
*
|
|
1981
2126
|
* @param {string} path The path to the QVD file.
|
|
1982
2127
|
* @param {Object} [options] Optional writing options.
|
|
1983
|
-
* @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.
|
|
1984
2132
|
* @param {Function} [options.onProgress] Optional progress callback function that receives progress updates during write operations.
|
|
1985
2133
|
*/
|
|
1986
2134
|
async toQvd(path3, options = {}) {
|
|
@@ -1996,11 +2144,16 @@ var init_QvdDataFrame = __esm({
|
|
|
1996
2144
|
*
|
|
1997
2145
|
* @param {string} path The path to the QVD file.
|
|
1998
2146
|
* @param {Object} [options] Optional loading options.
|
|
1999
|
-
* @param {number|null} [options.maxRows] The maximum number of rows to load.
|
|
2000
|
-
*
|
|
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.
|
|
2001
2153
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage
|
|
2002
2154
|
* of available memory (or V8 heap limit, whichever is smaller) can be used. Default is 0.3 (30%).
|
|
2003
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.
|
|
2004
2157
|
* @return {Promise<QvdDataFrame>} The data frame of the QVD file.
|
|
2005
2158
|
*/
|
|
2006
2159
|
static async fromQvd(path3, options = {}) {
|