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 CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var path2 = require('path');
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 path2__default = /*#__PURE__*/_interopDefault(path2);
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 = path2__default.default.resolve(baseDir);
274
- const resolvedPath = path2__default.default.resolve(filePath);
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 || path2__default.default.basename(this._path, path2__default.default.extname(this._path)),
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: path2__default.default.basename(this._path, path2__default.default.extname(this._path)),
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, this._df.columns.indexOf(column)).reduce((sum, metadata) => sum + metadata[1], 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 bitWidth = this._indexTable.length > 0 ? Math.max(
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[this._df.columns.indexOf(column)];
587
- const paddedBitString = bitString.padStart(bitWidth, "0");
588
- indices[this._df.columns.indexOf(column)] = paddedBitString;
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
- const maxReasonableOverage = 100 * 1024 * 1024;
855
- if (indexTableLength > bufferLength + maxReasonableOverage) {
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
- bufferSize: bufferLength,
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
- headerBuffer = Buffer.concat([headerBuffer, chunk]);
1250
- headerDelimiterIndex = headerBuffer.indexOf(HEADER_DELIMITER);
1251
- if (headerDelimiterIndex !== -1) {
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
- if (error && typeof error === "object" && "code" in error && error.code !== "ERR_STREAM_PREMATURE_CLOSE") {
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
- this._buffer = Buffer.allocUnsafe(totalBytesToRead);
1291
- await fd.read(this._buffer, 0, totalBytesToRead, 0);
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 -= bias;
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 must be within this directory.
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. If not specified, all rows are loaded.
2003
- * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path must be within this directory.
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 = {}) {