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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'path';
|
|
2
2
|
import fs2 from 'fs';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import xml2 from 'xml2js';
|
|
@@ -250,20 +250,42 @@ var init_QvdSymbol = __esm({
|
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
});
|
|
253
|
+
function isWithinDirectory(resolvedBaseDir, resolvedPath) {
|
|
254
|
+
const isCaseInsensitiveFS = process.platform === "win32" || process.platform === "darwin";
|
|
255
|
+
const base = isCaseInsensitiveFS ? resolvedBaseDir.toLowerCase() : resolvedBaseDir;
|
|
256
|
+
const target = isCaseInsensitiveFS ? resolvedPath.toLowerCase() : resolvedPath;
|
|
257
|
+
const relative = path.relative(base, target);
|
|
258
|
+
if (relative === "") {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (path.isAbsolute(relative)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`);
|
|
265
|
+
}
|
|
253
266
|
function validatePath(filePath, allowedDir) {
|
|
267
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
268
|
+
throw new QvdValidationError("filePath must be a non-empty string", {
|
|
269
|
+
provided: filePath,
|
|
270
|
+
type: typeof filePath
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (allowedDir !== void 0 && allowedDir !== null && typeof allowedDir !== "string") {
|
|
274
|
+
throw new QvdValidationError("allowedDir must be a string, null or undefined", {
|
|
275
|
+
provided: allowedDir,
|
|
276
|
+
type: typeof allowedDir
|
|
277
|
+
});
|
|
278
|
+
}
|
|
254
279
|
if (filePath.includes("\0")) {
|
|
255
280
|
throw new QvdSecurityError("Path traversal detected: Null byte in path", {
|
|
256
281
|
path: filePath,
|
|
257
282
|
reason: "null_byte"
|
|
258
283
|
});
|
|
259
284
|
}
|
|
285
|
+
const resolvedPath = path.resolve(filePath);
|
|
260
286
|
const baseDir = allowedDir || process.cwd();
|
|
261
|
-
const resolvedBaseDir =
|
|
262
|
-
|
|
263
|
-
const isCaseInsensitiveFS = process.platform === "win32" || process.platform === "darwin";
|
|
264
|
-
const baseForComparison = isCaseInsensitiveFS ? resolvedBaseDir.toLowerCase() : resolvedBaseDir;
|
|
265
|
-
const pathForComparison = isCaseInsensitiveFS ? resolvedPath.toLowerCase() : resolvedPath;
|
|
266
|
-
if (!pathForComparison.startsWith(baseForComparison + path2.sep) && pathForComparison !== baseForComparison) {
|
|
287
|
+
const resolvedBaseDir = path.resolve(baseDir);
|
|
288
|
+
if (!isWithinDirectory(resolvedBaseDir, resolvedPath)) {
|
|
267
289
|
throw new QvdSecurityError("Path traversal detected: Access denied", {
|
|
268
290
|
path: filePath,
|
|
269
291
|
resolvedPath,
|
|
@@ -297,7 +319,9 @@ var init_QvdFileWriter = __esm({
|
|
|
297
319
|
* @param {QvdDataFrame} df The data frame to write to the QVD file.
|
|
298
320
|
* @param {Object} [options={}] Options for the writer.
|
|
299
321
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
300
|
-
* path must be within this directory. Defaults to current working directory.
|
|
322
|
+
* path must be within this directory. Defaults to the current working directory. To permit
|
|
323
|
+
* an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
|
|
324
|
+
* empty value falls back to the working directory rather than removing the restriction.
|
|
301
325
|
* @param {Function} [options.onProgress] Optional progress callback function.
|
|
302
326
|
*/
|
|
303
327
|
constructor(filePath, df, options = {}) {
|
|
@@ -369,7 +393,7 @@ var init_QvdFileWriter = __esm({
|
|
|
369
393
|
SourceFileUtcTime: existingMetadata.SourceFileUtcTime || "",
|
|
370
394
|
SourceFileSize: existingMetadata.SourceFileSize || -1,
|
|
371
395
|
StaleUtcTime: existingMetadata.StaleUtcTime || "",
|
|
372
|
-
TableName: existingMetadata.TableName ||
|
|
396
|
+
TableName: existingMetadata.TableName || path.basename(this._path, path.extname(this._path)),
|
|
373
397
|
Compression: existingMetadata.Compression || "",
|
|
374
398
|
Comment: existingMetadata.Comment || "",
|
|
375
399
|
EncryptionInfo: existingMetadata.EncryptionInfo || "",
|
|
@@ -389,7 +413,7 @@ var init_QvdFileWriter = __esm({
|
|
|
389
413
|
SourceFileUtcTime: "",
|
|
390
414
|
SourceFileSize: -1,
|
|
391
415
|
StaleUtcTime: "",
|
|
392
|
-
TableName:
|
|
416
|
+
TableName: path.basename(this._path, path.extname(this._path)),
|
|
393
417
|
Compression: "",
|
|
394
418
|
Comment: "",
|
|
395
419
|
EncryptionInfo: "",
|
|
@@ -559,21 +583,22 @@ var init_QvdFileWriter = __esm({
|
|
|
559
583
|
this._emitProgress("index-table", processedRows, numRows);
|
|
560
584
|
}
|
|
561
585
|
});
|
|
562
|
-
this._df.columns.forEach((column) => {
|
|
563
|
-
const bitOffset = this._indexTableMetadata?.slice(0,
|
|
586
|
+
this._df.columns.forEach((column, columnIndex) => {
|
|
587
|
+
const bitOffset = this._indexTableMetadata?.slice(0, columnIndex).reduce((sum, metadata) => sum + metadata[1], 0);
|
|
564
588
|
assert(this._indexTable, "The QVD file header has not been parsed.");
|
|
565
|
-
const
|
|
566
|
-
...this._indexTable.map(
|
|
567
|
-
(indices) => indices[this._df.columns.indexOf(column)].length
|
|
568
|
-
)
|
|
569
|
-
) : 0;
|
|
570
|
-
const fieldContainsNull = this._symbolTableMetadata?.[this._df.columns.indexOf(column)][2];
|
|
589
|
+
const fieldContainsNull = this._symbolTableMetadata?.[columnIndex][2];
|
|
571
590
|
const bias = fieldContainsNull ? -2 : 0;
|
|
591
|
+
const symbolCount = this._symbolTable?.[columnIndex].length ?? 0;
|
|
592
|
+
const maxStoredIndex = symbolCount === 0 ? 0 : symbolCount - 1 + (fieldContainsNull ? 2 : 0);
|
|
593
|
+
const bitWidth = maxStoredIndex === 0 ? 0 : 32 - Math.clz32(maxStoredIndex);
|
|
572
594
|
this._indexTableMetadata?.push([bitOffset, bitWidth, bias]);
|
|
573
595
|
this._indexTable.forEach((indices) => {
|
|
574
|
-
const bitString = indices[
|
|
575
|
-
|
|
576
|
-
|
|
596
|
+
const bitString = indices[columnIndex];
|
|
597
|
+
assert(
|
|
598
|
+
bitString === "0" || bitString.length <= bitWidth,
|
|
599
|
+
`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.`
|
|
600
|
+
);
|
|
601
|
+
indices[columnIndex] = bitWidth === 0 ? "" : bitString.padStart(bitWidth, "0");
|
|
577
602
|
});
|
|
578
603
|
});
|
|
579
604
|
this._indexBuffer = Buffer.concat(
|
|
@@ -581,6 +606,9 @@ var init_QvdFileWriter = __esm({
|
|
|
581
606
|
this._indexTable.map((indices) => {
|
|
582
607
|
indices.reverse();
|
|
583
608
|
const bits = indices.join("");
|
|
609
|
+
if (bits.length === 0) {
|
|
610
|
+
return Buffer.from(Uint8Array.from([0]));
|
|
611
|
+
}
|
|
584
612
|
const paddingWidth = (8 - bits.length % 8) % 8;
|
|
585
613
|
const paddedBits = bits.padStart(bits.length + paddingWidth, "0");
|
|
586
614
|
const bytes = paddedBits.match(/.{1,8}/g)?.map((byte) => parseInt(byte, 2));
|
|
@@ -664,6 +692,9 @@ function estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
|
|
|
664
692
|
return keptSymbolsMemory + skippedSymbolsMemory;
|
|
665
693
|
}
|
|
666
694
|
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.3) {
|
|
695
|
+
if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
|
|
696
|
+
throw new QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", { safetyFactor });
|
|
697
|
+
}
|
|
667
698
|
const availableMemory = os.freemem();
|
|
668
699
|
const heapLimit = getHeapLimit();
|
|
669
700
|
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
@@ -790,7 +821,7 @@ function validateFieldMetadata(field, symbolBufferLength, filePath) {
|
|
|
790
821
|
});
|
|
791
822
|
}
|
|
792
823
|
}
|
|
793
|
-
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath) {
|
|
824
|
+
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath, fileSize = null) {
|
|
794
825
|
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
795
826
|
throw new QvdCorruptedError("Invalid record byte size", {
|
|
796
827
|
recordSize,
|
|
@@ -836,16 +867,40 @@ function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, ind
|
|
|
836
867
|
stage: "parseIndexTable"
|
|
837
868
|
});
|
|
838
869
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
throw new QvdCorruptedError("Index table length unreasonably large", {
|
|
870
|
+
if (indexTableLength !== totalRows * recordSize) {
|
|
871
|
+
throw new QvdCorruptedError("Index table length inconsistent with record count", {
|
|
842
872
|
indexTableLength,
|
|
843
|
-
|
|
873
|
+
expectedLength: totalRows * recordSize,
|
|
874
|
+
totalRows,
|
|
875
|
+
recordSize,
|
|
844
876
|
file: filePath,
|
|
845
877
|
stage: "parseIndexTable"
|
|
846
878
|
});
|
|
847
879
|
}
|
|
880
|
+
if (fileSize !== null) {
|
|
881
|
+
if (indexTableOffset + indexTableLength > fileSize) {
|
|
882
|
+
throw new QvdCorruptedError("Index table extends beyond the end of the file", {
|
|
883
|
+
indexTableOffset,
|
|
884
|
+
indexTableLength,
|
|
885
|
+
fileSize,
|
|
886
|
+
file: filePath,
|
|
887
|
+
stage: "parseIndexTable"
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
848
891
|
const requiredIndexBytes = rowsToLoad * recordSize;
|
|
892
|
+
if (indexTableOffset + requiredIndexBytes > bufferLength) {
|
|
893
|
+
throw new QvdCorruptedError("Index table truncated", {
|
|
894
|
+
indexTableOffset,
|
|
895
|
+
requiredBytes: requiredIndexBytes,
|
|
896
|
+
availableBytes: Math.max(0, bufferLength - indexTableOffset),
|
|
897
|
+
rowsToLoad,
|
|
898
|
+
recordSize,
|
|
899
|
+
bufferSize: bufferLength,
|
|
900
|
+
file: filePath,
|
|
901
|
+
stage: "parseIndexTable"
|
|
902
|
+
});
|
|
903
|
+
}
|
|
849
904
|
if (indexTableLength < requiredIndexBytes) {
|
|
850
905
|
throw new QvdCorruptedError("Index table length smaller than required", {
|
|
851
906
|
indexTableLength,
|
|
@@ -1156,7 +1211,7 @@ var QvdFileReader_exports = {};
|
|
|
1156
1211
|
__export(QvdFileReader_exports, {
|
|
1157
1212
|
QvdFileReader: () => QvdFileReader
|
|
1158
1213
|
});
|
|
1159
|
-
var QvdFileReader;
|
|
1214
|
+
var MAX_HEADER_SIZE, READ_CHUNK_SIZE, QvdFileReader;
|
|
1160
1215
|
var init_QvdFileReader = __esm({
|
|
1161
1216
|
"src/QvdFileReader.js"() {
|
|
1162
1217
|
init_QvdDataFrame();
|
|
@@ -1166,6 +1221,8 @@ var init_QvdFileReader = __esm({
|
|
|
1166
1221
|
init_memoryUtils();
|
|
1167
1222
|
init_validationUtils();
|
|
1168
1223
|
init_symbolParser();
|
|
1224
|
+
MAX_HEADER_SIZE = 16 * 1024 * 1024;
|
|
1225
|
+
READ_CHUNK_SIZE = 512 * 1024 * 1024;
|
|
1169
1226
|
QvdFileReader = class {
|
|
1170
1227
|
/**
|
|
1171
1228
|
* Constructs a new QVD file parser.
|
|
@@ -1173,7 +1230,9 @@ var init_QvdFileReader = __esm({
|
|
|
1173
1230
|
* @param {string} filePath The path to the QVD file to load.
|
|
1174
1231
|
* @param {Object} [options={}] Options for the reader.
|
|
1175
1232
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
1176
|
-
* path must be within this directory. Defaults to current working directory.
|
|
1233
|
+
* path must be within this directory. Defaults to the current working directory. To permit
|
|
1234
|
+
* an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
|
|
1235
|
+
* empty value falls back to the working directory rather than removing the restriction.
|
|
1177
1236
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
|
|
1178
1237
|
* what percentage of available memory (or V8 heap limit, whichever is smaller) can be used
|
|
1179
1238
|
* for loading QVD files. Default is 0.3 (30%). Increase for larger heap configurations.
|
|
@@ -1189,6 +1248,7 @@ var init_QvdFileReader = __esm({
|
|
|
1189
1248
|
this._header = null;
|
|
1190
1249
|
this._symbolTable = null;
|
|
1191
1250
|
this._indexTable = null;
|
|
1251
|
+
this._fileSize = null;
|
|
1192
1252
|
}
|
|
1193
1253
|
/**
|
|
1194
1254
|
* Reads the binary data of the QVD file.
|
|
@@ -1220,6 +1280,7 @@ var init_QvdFileReader = __esm({
|
|
|
1220
1280
|
async _readData(maxRows = null) {
|
|
1221
1281
|
if (maxRows === null) {
|
|
1222
1282
|
this._buffer = await fs2.promises.readFile(this._path);
|
|
1283
|
+
this._fileSize = this._buffer.length;
|
|
1223
1284
|
return;
|
|
1224
1285
|
}
|
|
1225
1286
|
const HEADER_DELIMITER = "\r\n\0";
|
|
@@ -1227,19 +1288,41 @@ var init_QvdFileReader = __esm({
|
|
|
1227
1288
|
const stream = fs2.createReadStream(this._path, {
|
|
1228
1289
|
highWaterMark: CHUNK_SIZE
|
|
1229
1290
|
});
|
|
1291
|
+
const headerChunks = [];
|
|
1292
|
+
let headerBytes = 0;
|
|
1293
|
+
let tail = Buffer.alloc(0);
|
|
1230
1294
|
let headerBuffer = Buffer.alloc(0);
|
|
1231
1295
|
let headerDelimiterIndex = -1;
|
|
1232
1296
|
try {
|
|
1233
1297
|
for await (const chunk of stream) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1298
|
+
const chunkStart = headerBytes;
|
|
1299
|
+
const searchBuffer = tail.length > 0 ? Buffer.concat([tail, chunk]) : chunk;
|
|
1300
|
+
const foundInSearch = searchBuffer.indexOf(HEADER_DELIMITER);
|
|
1301
|
+
headerChunks.push(chunk);
|
|
1302
|
+
headerBytes += chunk.length;
|
|
1303
|
+
if (foundInSearch !== -1) {
|
|
1304
|
+
headerDelimiterIndex = chunkStart - tail.length + foundInSearch;
|
|
1237
1305
|
stream.destroy();
|
|
1238
1306
|
break;
|
|
1239
1307
|
}
|
|
1308
|
+
tail = Buffer.from(searchBuffer.subarray(-(HEADER_DELIMITER.length - 1)));
|
|
1309
|
+
if (headerBytes > MAX_HEADER_SIZE) {
|
|
1310
|
+
stream.destroy();
|
|
1311
|
+
throw new QvdCorruptedError(
|
|
1312
|
+
`The XML header delimiter was not found within the first ${MAX_HEADER_SIZE / (1024 * 1024)}MB of the file.`,
|
|
1313
|
+
{
|
|
1314
|
+
file: this._path,
|
|
1315
|
+
bytesSearched: headerBytes,
|
|
1316
|
+
maxHeaderSize: MAX_HEADER_SIZE,
|
|
1317
|
+
stage: "readData"
|
|
1318
|
+
}
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1240
1321
|
}
|
|
1241
1322
|
} catch (error) {
|
|
1242
|
-
|
|
1323
|
+
const isExpectedEarlyClose = headerDelimiterIndex !== -1 && error !== null && typeof error === "object" && /** @type {{code?: unknown}} */
|
|
1324
|
+
error.code === "ERR_STREAM_PREMATURE_CLOSE";
|
|
1325
|
+
if (!isExpectedEarlyClose) {
|
|
1243
1326
|
throw error;
|
|
1244
1327
|
}
|
|
1245
1328
|
}
|
|
@@ -1252,6 +1335,7 @@ var init_QvdFileReader = __esm({
|
|
|
1252
1335
|
}
|
|
1253
1336
|
);
|
|
1254
1337
|
}
|
|
1338
|
+
headerBuffer = Buffer.concat(headerChunks);
|
|
1255
1339
|
const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
|
|
1256
1340
|
const headerXml = headerBuffer.subarray(0, headerEndIndex).toString();
|
|
1257
1341
|
const headerObj = await xml2.parseStringPromise(headerXml, { explicitArray: false });
|
|
@@ -1268,12 +1352,50 @@ var init_QvdFileReader = __esm({
|
|
|
1268
1352
|
const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1269
1353
|
const rowsToLoad = Math.min(maxRows, totalRows);
|
|
1270
1354
|
validateSymbolTableSizeEarly(symbolTableLength, this._path);
|
|
1355
|
+
for (const [name, value] of [
|
|
1356
|
+
["Offset", symbolTableLength],
|
|
1357
|
+
["RecordByteSize", recordSize],
|
|
1358
|
+
["NoOfRecords", totalRows]
|
|
1359
|
+
]) {
|
|
1360
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
|
1361
|
+
throw new QvdCorruptedError(`Invalid header value: ${name}`, {
|
|
1362
|
+
name,
|
|
1363
|
+
value,
|
|
1364
|
+
file: this._path,
|
|
1365
|
+
stage: "readData"
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1271
1369
|
const indexTableBytesToRead = rowsToLoad * recordSize;
|
|
1272
1370
|
const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
|
|
1273
1371
|
const fd = await fs2.promises.open(this._path, "r");
|
|
1274
1372
|
try {
|
|
1275
|
-
|
|
1276
|
-
|
|
1373
|
+
const { size: fileSize } = await fd.stat();
|
|
1374
|
+
this._fileSize = fileSize;
|
|
1375
|
+
if (totalBytesToRead > fileSize) {
|
|
1376
|
+
throw new QvdCorruptedError("The file is shorter than its header claims.", {
|
|
1377
|
+
file: this._path,
|
|
1378
|
+
fileSize,
|
|
1379
|
+
requiredBytes: totalBytesToRead,
|
|
1380
|
+
stage: "readData"
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
this._buffer = Buffer.alloc(totalBytesToRead);
|
|
1384
|
+
let position = 0;
|
|
1385
|
+
while (position < totalBytesToRead) {
|
|
1386
|
+
const length = Math.min(READ_CHUNK_SIZE, totalBytesToRead - position);
|
|
1387
|
+
const { bytesRead } = await fd.read(this._buffer, position, length, position);
|
|
1388
|
+
if (bytesRead === 0) {
|
|
1389
|
+
throw new QvdCorruptedError("Unexpected end of file while reading QVD data.", {
|
|
1390
|
+
file: this._path,
|
|
1391
|
+
fileSize,
|
|
1392
|
+
bytesRead: position,
|
|
1393
|
+
requiredBytes: totalBytesToRead,
|
|
1394
|
+
stage: "readData"
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
position += bytesRead;
|
|
1398
|
+
}
|
|
1277
1399
|
} finally {
|
|
1278
1400
|
await fd.close();
|
|
1279
1401
|
}
|
|
@@ -1375,7 +1497,10 @@ var init_QvdFileReader = __esm({
|
|
|
1375
1497
|
} else {
|
|
1376
1498
|
symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
|
|
1377
1499
|
}
|
|
1378
|
-
symbolIndex
|
|
1500
|
+
symbolIndex += bias;
|
|
1501
|
+
if (symbolIndex < 0) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1379
1504
|
const fieldName = field["FieldName"];
|
|
1380
1505
|
symbolUsage.get(fieldName)?.add(symbolIndex);
|
|
1381
1506
|
});
|
|
@@ -1480,7 +1605,8 @@ var init_QvdFileReader = __esm({
|
|
|
1480
1605
|
this._indexTableOffset,
|
|
1481
1606
|
this._buffer.length,
|
|
1482
1607
|
rowsToLoad,
|
|
1483
|
-
this._path
|
|
1608
|
+
this._path,
|
|
1609
|
+
this._fileSize
|
|
1484
1610
|
);
|
|
1485
1611
|
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1486
1612
|
for (const field of fields) {
|
|
@@ -1516,14 +1642,33 @@ var init_QvdFileReader = __esm({
|
|
|
1516
1642
|
});
|
|
1517
1643
|
this._indexTable.push(symbolIndices);
|
|
1518
1644
|
}
|
|
1645
|
+
if (this._indexTable.length !== rowsToLoad) {
|
|
1646
|
+
throw new QvdCorruptedError("Index table contains fewer records than expected", {
|
|
1647
|
+
expectedRows: rowsToLoad,
|
|
1648
|
+
actualRows: this._indexTable.length,
|
|
1649
|
+
totalRows,
|
|
1650
|
+
recordSize,
|
|
1651
|
+
file: this._path,
|
|
1652
|
+
stage: "parseIndexTable"
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1519
1655
|
}
|
|
1520
1656
|
/**
|
|
1521
1657
|
* Loads the QVD file into memory and parses it.
|
|
1522
1658
|
*
|
|
1523
1659
|
* @param {number|null} maxRows The maximum number of rows to load. If null, all rows are loaded.
|
|
1660
|
+
* Must be a non-negative integer when given.
|
|
1661
|
+
* @throws {QvdValidationError} If maxRows is neither null nor a non-negative integer.
|
|
1524
1662
|
* @return {Promise<QvdDataFrame>} The loaded QVD file.
|
|
1525
1663
|
*/
|
|
1526
1664
|
async load(maxRows = null) {
|
|
1665
|
+
if (maxRows !== null && (typeof maxRows !== "number" || !Number.isInteger(maxRows) || maxRows < 0)) {
|
|
1666
|
+
throw new QvdValidationError("maxRows must be a non-negative integer, or null to load all rows", {
|
|
1667
|
+
provided: maxRows,
|
|
1668
|
+
type: typeof maxRows,
|
|
1669
|
+
file: this._path
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1527
1672
|
await this._readData(maxRows);
|
|
1528
1673
|
await this._parseHeader();
|
|
1529
1674
|
let symbolsToKeep = null;
|
|
@@ -1558,7 +1703,7 @@ var init_QvdFileReader = __esm({
|
|
|
1558
1703
|
const symbol = this._symbolTable?.[fieldIndex]?.[symbolIndex];
|
|
1559
1704
|
const value = symbol?.toPrimaryValue();
|
|
1560
1705
|
if (typeof value === "string") {
|
|
1561
|
-
if (!isNaN(Number(value))) {
|
|
1706
|
+
if (value.trim() !== "" && !isNaN(Number(value))) {
|
|
1562
1707
|
return Number(value);
|
|
1563
1708
|
}
|
|
1564
1709
|
}
|
|
@@ -1968,7 +2113,10 @@ var init_QvdDataFrame = __esm({
|
|
|
1968
2113
|
*
|
|
1969
2114
|
* @param {string} path The path to the QVD file.
|
|
1970
2115
|
* @param {Object} [options] Optional writing options.
|
|
1971
|
-
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2116
|
+
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2117
|
+
* must be within this directory. Defaults to the current working directory. To permit an entire
|
|
2118
|
+
* volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
|
|
2119
|
+
* back to the working directory rather than removing the restriction.
|
|
1972
2120
|
* @param {Function} [options.onProgress] Optional progress callback function that receives progress updates during write operations.
|
|
1973
2121
|
*/
|
|
1974
2122
|
async toQvd(path3, options = {}) {
|
|
@@ -1984,11 +2132,16 @@ var init_QvdDataFrame = __esm({
|
|
|
1984
2132
|
*
|
|
1985
2133
|
* @param {string} path The path to the QVD file.
|
|
1986
2134
|
* @param {Object} [options] Optional loading options.
|
|
1987
|
-
* @param {number|null} [options.maxRows] The maximum number of rows to load.
|
|
1988
|
-
*
|
|
2135
|
+
* @param {number|null} [options.maxRows] The maximum number of rows to load. Must be a non-negative
|
|
2136
|
+
* integer; if not specified or null, all rows are loaded. Anything else throws a QvdValidationError.
|
|
2137
|
+
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2138
|
+
* must be within this directory. Defaults to the current working directory. To permit an entire
|
|
2139
|
+
* volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
|
|
2140
|
+
* back to the working directory rather than removing the restriction.
|
|
1989
2141
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage
|
|
1990
2142
|
* of available memory (or V8 heap limit, whichever is smaller) can be used. Default is 0.3 (30%).
|
|
1991
2143
|
* Increase this (e.g., to 0.5 or 0.7) when running with larger heap sizes via --max-old-space-size.
|
|
2144
|
+
* @throws {QvdValidationError} If options.maxRows is neither null/undefined nor a non-negative integer.
|
|
1992
2145
|
* @return {Promise<QvdDataFrame>} The data frame of the QVD file.
|
|
1993
2146
|
*/
|
|
1994
2147
|
static async fromQvd(path3, options = {}) {
|