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.js CHANGED
@@ -1,4 +1,4 @@
1
- import path2 from 'path';
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 = path2.resolve(baseDir);
262
- const resolvedPath = path2.resolve(filePath);
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 || path2.basename(this._path, path2.extname(this._path)),
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: path2.basename(this._path, path2.extname(this._path)),
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, this._df.columns.indexOf(column)).reduce((sum, metadata) => sum + metadata[1], 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 bitWidth = this._indexTable.length > 0 ? Math.max(
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[this._df.columns.indexOf(column)];
575
- const paddedBitString = bitString.padStart(bitWidth, "0");
576
- indices[this._df.columns.indexOf(column)] = paddedBitString;
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
- const maxReasonableOverage = 100 * 1024 * 1024;
840
- if (indexTableLength > bufferLength + maxReasonableOverage) {
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
- bufferSize: bufferLength,
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
- headerBuffer = Buffer.concat([headerBuffer, chunk]);
1235
- headerDelimiterIndex = headerBuffer.indexOf(HEADER_DELIMITER);
1236
- if (headerDelimiterIndex !== -1) {
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
- if (error && typeof error === "object" && "code" in error && error.code !== "ERR_STREAM_PREMATURE_CLOSE") {
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
- this._buffer = Buffer.allocUnsafe(totalBytesToRead);
1276
- await fd.read(this._buffer, 0, totalBytesToRead, 0);
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 -= bias;
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 must be within this directory.
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. If not specified, all rows are loaded.
1988
- * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path must be within this directory.
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 = {}) {