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 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));
@@ -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
- const maxReasonableOverage = 100 * 1024 * 1024;
852
- if (indexTableLength > bufferLength + maxReasonableOverage) {
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
- bufferSize: bufferLength,
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
- headerBuffer = Buffer.concat([headerBuffer, chunk]);
1247
- headerDelimiterIndex = headerBuffer.indexOf(HEADER_DELIMITER);
1248
- 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;
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
- 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) {
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
- this._buffer = Buffer.allocUnsafe(totalBytesToRead);
1288
- 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
+ }
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 -= bias;
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 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.
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. If not specified, all rows are loaded.
2000
- * @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.
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 = {}) {