qvdjs 0.9.0 → 0.9.2
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 +256 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +254 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import xml2 from 'xml2js';
|
|
5
5
|
import assert from 'assert';
|
|
@@ -250,25 +250,101 @@ var init_QvdSymbol = __esm({
|
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
});
|
|
253
|
+
function isWithinDirectoryLexically(resolvedBaseDir, resolvedPath) {
|
|
254
|
+
const isCaseInsensitiveFS = process.platform === "win32";
|
|
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
|
+
}
|
|
266
|
+
function resolveDeepestExisting(target) {
|
|
267
|
+
let current = target;
|
|
268
|
+
for (; ; ) {
|
|
269
|
+
try {
|
|
270
|
+
return fs.realpathSync(current);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
const code = (
|
|
273
|
+
/** @type {{code?: string}} */
|
|
274
|
+
error?.code
|
|
275
|
+
);
|
|
276
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const parent = path.dirname(current);
|
|
280
|
+
if (parent === current) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
current = parent;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function isWithinDirectoryOnDisk(resolvedBaseDir, resolvedPath) {
|
|
288
|
+
let baseStat;
|
|
289
|
+
try {
|
|
290
|
+
baseStat = fs.statSync(fs.realpathSync(resolvedBaseDir));
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
let current = resolveDeepestExisting(resolvedPath);
|
|
295
|
+
if (current === null) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
for (; ; ) {
|
|
299
|
+
let stat;
|
|
300
|
+
try {
|
|
301
|
+
stat = fs.statSync(current);
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
if (stat.dev === baseStat.dev && stat.ino === baseStat.ino) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
const parent = path.dirname(current);
|
|
309
|
+
if (parent === current) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
current = parent;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
253
315
|
function validatePath(filePath, allowedDir) {
|
|
316
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
317
|
+
throw new QvdValidationError("filePath must be a non-empty string", {
|
|
318
|
+
provided: filePath,
|
|
319
|
+
type: typeof filePath
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (allowedDir !== void 0 && allowedDir !== null && typeof allowedDir !== "string") {
|
|
323
|
+
throw new QvdValidationError("allowedDir must be a string, null or undefined", {
|
|
324
|
+
provided: allowedDir,
|
|
325
|
+
type: typeof allowedDir
|
|
326
|
+
});
|
|
327
|
+
}
|
|
254
328
|
if (filePath.includes("\0")) {
|
|
255
329
|
throw new QvdSecurityError("Path traversal detected: Null byte in path", {
|
|
256
330
|
path: filePath,
|
|
257
331
|
reason: "null_byte"
|
|
258
332
|
});
|
|
259
333
|
}
|
|
334
|
+
const resolvedPath = path.resolve(filePath);
|
|
260
335
|
const baseDir = allowedDir || process.cwd();
|
|
261
|
-
const resolvedBaseDir =
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
const pathForComparison = isCaseInsensitiveFS ? resolvedPath.toLowerCase() : resolvedPath;
|
|
266
|
-
if (!pathForComparison.startsWith(baseForComparison + path2.sep) && pathForComparison !== baseForComparison) {
|
|
336
|
+
const resolvedBaseDir = path.resolve(baseDir);
|
|
337
|
+
const onDisk = isWithinDirectoryOnDisk(resolvedBaseDir, resolvedPath);
|
|
338
|
+
const contained = onDisk === null ? isWithinDirectoryLexically(resolvedBaseDir, resolvedPath) : onDisk;
|
|
339
|
+
if (!contained) {
|
|
267
340
|
throw new QvdSecurityError("Path traversal detected: Access denied", {
|
|
268
341
|
path: filePath,
|
|
269
342
|
resolvedPath,
|
|
270
343
|
allowedDir: resolvedBaseDir,
|
|
271
|
-
reason: "outside_allowed_directory"
|
|
344
|
+
reason: "outside_allowed_directory",
|
|
345
|
+
// Says which check refused, so a rejection of a path that looks contained is traceable to
|
|
346
|
+
// a symlink or a case difference rather than looking like a bug.
|
|
347
|
+
check: onDisk === null ? "lexical" : "filesystem"
|
|
272
348
|
});
|
|
273
349
|
}
|
|
274
350
|
return resolvedPath;
|
|
@@ -297,7 +373,10 @@ var init_QvdFileWriter = __esm({
|
|
|
297
373
|
* @param {QvdDataFrame} df The data frame to write to the QVD file.
|
|
298
374
|
* @param {Object} [options={}] Options for the writer.
|
|
299
375
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
300
|
-
* path must be within this directory
|
|
376
|
+
* path must be within this directory, with symlinks resolved first, so a link inside it that
|
|
377
|
+
* points outside it is rejected. Defaults to the current working directory. To permit
|
|
378
|
+
* an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
|
|
379
|
+
* empty value falls back to the working directory rather than removing the restriction.
|
|
301
380
|
* @param {Function} [options.onProgress] Optional progress callback function.
|
|
302
381
|
*/
|
|
303
382
|
constructor(filePath, df, options = {}) {
|
|
@@ -343,7 +422,7 @@ var init_QvdFileWriter = __esm({
|
|
|
343
422
|
const headerBuffer = Buffer.concat([Buffer.from(this._header, "utf-8"), Buffer.from([0])]);
|
|
344
423
|
let fd;
|
|
345
424
|
try {
|
|
346
|
-
fd = await
|
|
425
|
+
fd = await fs.promises.open(this._path, "w");
|
|
347
426
|
await fd.write(headerBuffer, 0, headerBuffer.length, 0);
|
|
348
427
|
await fd.write(this._symbolBuffer, 0, this._symbolBuffer.length, headerBuffer.length);
|
|
349
428
|
await fd.write(this._indexBuffer, 0, this._indexBuffer.length, headerBuffer.length + this._symbolBuffer.length);
|
|
@@ -369,7 +448,7 @@ var init_QvdFileWriter = __esm({
|
|
|
369
448
|
SourceFileUtcTime: existingMetadata.SourceFileUtcTime || "",
|
|
370
449
|
SourceFileSize: existingMetadata.SourceFileSize || -1,
|
|
371
450
|
StaleUtcTime: existingMetadata.StaleUtcTime || "",
|
|
372
|
-
TableName: existingMetadata.TableName ||
|
|
451
|
+
TableName: existingMetadata.TableName || path.basename(this._path, path.extname(this._path)),
|
|
373
452
|
Compression: existingMetadata.Compression || "",
|
|
374
453
|
Comment: existingMetadata.Comment || "",
|
|
375
454
|
EncryptionInfo: existingMetadata.EncryptionInfo || "",
|
|
@@ -389,7 +468,7 @@ var init_QvdFileWriter = __esm({
|
|
|
389
468
|
SourceFileUtcTime: "",
|
|
390
469
|
SourceFileSize: -1,
|
|
391
470
|
StaleUtcTime: "",
|
|
392
|
-
TableName:
|
|
471
|
+
TableName: path.basename(this._path, path.extname(this._path)),
|
|
393
472
|
Compression: "",
|
|
394
473
|
Comment: "",
|
|
395
474
|
EncryptionInfo: "",
|
|
@@ -559,21 +638,22 @@ var init_QvdFileWriter = __esm({
|
|
|
559
638
|
this._emitProgress("index-table", processedRows, numRows);
|
|
560
639
|
}
|
|
561
640
|
});
|
|
562
|
-
this._df.columns.forEach((column) => {
|
|
563
|
-
const bitOffset = this._indexTableMetadata?.slice(0,
|
|
641
|
+
this._df.columns.forEach((column, columnIndex) => {
|
|
642
|
+
const bitOffset = this._indexTableMetadata?.slice(0, columnIndex).reduce((sum, metadata) => sum + metadata[1], 0);
|
|
564
643
|
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];
|
|
644
|
+
const fieldContainsNull = this._symbolTableMetadata?.[columnIndex][2];
|
|
571
645
|
const bias = fieldContainsNull ? -2 : 0;
|
|
646
|
+
const symbolCount = this._symbolTable?.[columnIndex].length ?? 0;
|
|
647
|
+
const maxStoredIndex = symbolCount === 0 ? 0 : symbolCount - 1 + (fieldContainsNull ? 2 : 0);
|
|
648
|
+
const bitWidth = maxStoredIndex === 0 ? 0 : 32 - Math.clz32(maxStoredIndex);
|
|
572
649
|
this._indexTableMetadata?.push([bitOffset, bitWidth, bias]);
|
|
573
650
|
this._indexTable.forEach((indices) => {
|
|
574
|
-
const bitString = indices[
|
|
575
|
-
|
|
576
|
-
|
|
651
|
+
const bitString = indices[columnIndex];
|
|
652
|
+
assert(
|
|
653
|
+
bitString === "0" || bitString.length <= bitWidth,
|
|
654
|
+
`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.`
|
|
655
|
+
);
|
|
656
|
+
indices[columnIndex] = bitWidth === 0 ? "" : bitString.padStart(bitWidth, "0");
|
|
577
657
|
});
|
|
578
658
|
});
|
|
579
659
|
this._indexBuffer = Buffer.concat(
|
|
@@ -581,6 +661,9 @@ var init_QvdFileWriter = __esm({
|
|
|
581
661
|
this._indexTable.map((indices) => {
|
|
582
662
|
indices.reverse();
|
|
583
663
|
const bits = indices.join("");
|
|
664
|
+
if (bits.length === 0) {
|
|
665
|
+
return Buffer.from(Uint8Array.from([0]));
|
|
666
|
+
}
|
|
584
667
|
const paddingWidth = (8 - bits.length % 8) % 8;
|
|
585
668
|
const paddedBits = bits.padStart(bits.length + paddingWidth, "0");
|
|
586
669
|
const bytes = paddedBits.match(/.{1,8}/g)?.map((byte) => parseInt(byte, 2));
|
|
@@ -793,7 +876,7 @@ function validateFieldMetadata(field, symbolBufferLength, filePath) {
|
|
|
793
876
|
});
|
|
794
877
|
}
|
|
795
878
|
}
|
|
796
|
-
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath) {
|
|
879
|
+
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath, fileSize = null) {
|
|
797
880
|
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
798
881
|
throw new QvdCorruptedError("Invalid record byte size", {
|
|
799
882
|
recordSize,
|
|
@@ -839,16 +922,40 @@ function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, ind
|
|
|
839
922
|
stage: "parseIndexTable"
|
|
840
923
|
});
|
|
841
924
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
throw new QvdCorruptedError("Index table length unreasonably large", {
|
|
925
|
+
if (indexTableLength !== totalRows * recordSize) {
|
|
926
|
+
throw new QvdCorruptedError("Index table length inconsistent with record count", {
|
|
845
927
|
indexTableLength,
|
|
846
|
-
|
|
928
|
+
expectedLength: totalRows * recordSize,
|
|
929
|
+
totalRows,
|
|
930
|
+
recordSize,
|
|
847
931
|
file: filePath,
|
|
848
932
|
stage: "parseIndexTable"
|
|
849
933
|
});
|
|
850
934
|
}
|
|
935
|
+
if (fileSize !== null) {
|
|
936
|
+
if (indexTableOffset + indexTableLength > fileSize) {
|
|
937
|
+
throw new QvdCorruptedError("Index table extends beyond the end of the file", {
|
|
938
|
+
indexTableOffset,
|
|
939
|
+
indexTableLength,
|
|
940
|
+
fileSize,
|
|
941
|
+
file: filePath,
|
|
942
|
+
stage: "parseIndexTable"
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
851
946
|
const requiredIndexBytes = rowsToLoad * recordSize;
|
|
947
|
+
if (indexTableOffset + requiredIndexBytes > bufferLength) {
|
|
948
|
+
throw new QvdCorruptedError("Index table truncated", {
|
|
949
|
+
indexTableOffset,
|
|
950
|
+
requiredBytes: requiredIndexBytes,
|
|
951
|
+
availableBytes: Math.max(0, bufferLength - indexTableOffset),
|
|
952
|
+
rowsToLoad,
|
|
953
|
+
recordSize,
|
|
954
|
+
bufferSize: bufferLength,
|
|
955
|
+
file: filePath,
|
|
956
|
+
stage: "parseIndexTable"
|
|
957
|
+
});
|
|
958
|
+
}
|
|
852
959
|
if (indexTableLength < requiredIndexBytes) {
|
|
853
960
|
throw new QvdCorruptedError("Index table length smaller than required", {
|
|
854
961
|
indexTableLength,
|
|
@@ -1159,7 +1266,7 @@ var QvdFileReader_exports = {};
|
|
|
1159
1266
|
__export(QvdFileReader_exports, {
|
|
1160
1267
|
QvdFileReader: () => QvdFileReader
|
|
1161
1268
|
});
|
|
1162
|
-
var QvdFileReader;
|
|
1269
|
+
var MAX_HEADER_SIZE, READ_CHUNK_SIZE, QvdFileReader;
|
|
1163
1270
|
var init_QvdFileReader = __esm({
|
|
1164
1271
|
"src/QvdFileReader.js"() {
|
|
1165
1272
|
init_QvdDataFrame();
|
|
@@ -1169,6 +1276,8 @@ var init_QvdFileReader = __esm({
|
|
|
1169
1276
|
init_memoryUtils();
|
|
1170
1277
|
init_validationUtils();
|
|
1171
1278
|
init_symbolParser();
|
|
1279
|
+
MAX_HEADER_SIZE = 16 * 1024 * 1024;
|
|
1280
|
+
READ_CHUNK_SIZE = 512 * 1024 * 1024;
|
|
1172
1281
|
QvdFileReader = class {
|
|
1173
1282
|
/**
|
|
1174
1283
|
* Constructs a new QVD file parser.
|
|
@@ -1176,7 +1285,10 @@ var init_QvdFileReader = __esm({
|
|
|
1176
1285
|
* @param {string} filePath The path to the QVD file to load.
|
|
1177
1286
|
* @param {Object} [options={}] Options for the reader.
|
|
1178
1287
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
1179
|
-
* path must be within this directory
|
|
1288
|
+
* path must be within this directory, with symlinks resolved first, so a link inside it that
|
|
1289
|
+
* points outside it is rejected. Defaults to the current working directory. To permit
|
|
1290
|
+
* an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
|
|
1291
|
+
* empty value falls back to the working directory rather than removing the restriction.
|
|
1180
1292
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
|
|
1181
1293
|
* what percentage of available memory (or V8 heap limit, whichever is smaller) can be used
|
|
1182
1294
|
* for loading QVD files. Default is 0.3 (30%). Increase for larger heap configurations.
|
|
@@ -1192,6 +1304,7 @@ var init_QvdFileReader = __esm({
|
|
|
1192
1304
|
this._header = null;
|
|
1193
1305
|
this._symbolTable = null;
|
|
1194
1306
|
this._indexTable = null;
|
|
1307
|
+
this._fileSize = null;
|
|
1195
1308
|
}
|
|
1196
1309
|
/**
|
|
1197
1310
|
* Reads the binary data of the QVD file.
|
|
@@ -1222,27 +1335,50 @@ var init_QvdFileReader = __esm({
|
|
|
1222
1335
|
*/
|
|
1223
1336
|
async _readData(maxRows = null) {
|
|
1224
1337
|
if (maxRows === null) {
|
|
1225
|
-
this._buffer = await
|
|
1338
|
+
this._buffer = await fs.promises.readFile(this._path);
|
|
1339
|
+
this._fileSize = this._buffer.length;
|
|
1226
1340
|
return;
|
|
1227
1341
|
}
|
|
1228
1342
|
const HEADER_DELIMITER = "\r\n\0";
|
|
1229
1343
|
const CHUNK_SIZE = 64 * 1024;
|
|
1230
|
-
const stream =
|
|
1344
|
+
const stream = fs.createReadStream(this._path, {
|
|
1231
1345
|
highWaterMark: CHUNK_SIZE
|
|
1232
1346
|
});
|
|
1347
|
+
const headerChunks = [];
|
|
1348
|
+
let headerBytes = 0;
|
|
1349
|
+
let tail = Buffer.alloc(0);
|
|
1233
1350
|
let headerBuffer = Buffer.alloc(0);
|
|
1234
1351
|
let headerDelimiterIndex = -1;
|
|
1235
1352
|
try {
|
|
1236
1353
|
for await (const chunk of stream) {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1354
|
+
const chunkStart = headerBytes;
|
|
1355
|
+
const searchBuffer = tail.length > 0 ? Buffer.concat([tail, chunk]) : chunk;
|
|
1356
|
+
const foundInSearch = searchBuffer.indexOf(HEADER_DELIMITER);
|
|
1357
|
+
headerChunks.push(chunk);
|
|
1358
|
+
headerBytes += chunk.length;
|
|
1359
|
+
if (foundInSearch !== -1) {
|
|
1360
|
+
headerDelimiterIndex = chunkStart - tail.length + foundInSearch;
|
|
1240
1361
|
stream.destroy();
|
|
1241
1362
|
break;
|
|
1242
1363
|
}
|
|
1364
|
+
tail = Buffer.from(searchBuffer.subarray(-(HEADER_DELIMITER.length - 1)));
|
|
1365
|
+
if (headerBytes > MAX_HEADER_SIZE) {
|
|
1366
|
+
stream.destroy();
|
|
1367
|
+
throw new QvdCorruptedError(
|
|
1368
|
+
`The XML header delimiter was not found within the first ${MAX_HEADER_SIZE / (1024 * 1024)}MB of the file.`,
|
|
1369
|
+
{
|
|
1370
|
+
file: this._path,
|
|
1371
|
+
bytesSearched: headerBytes,
|
|
1372
|
+
maxHeaderSize: MAX_HEADER_SIZE,
|
|
1373
|
+
stage: "readData"
|
|
1374
|
+
}
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1243
1377
|
}
|
|
1244
1378
|
} catch (error) {
|
|
1245
|
-
|
|
1379
|
+
const isExpectedEarlyClose = headerDelimiterIndex !== -1 && error !== null && typeof error === "object" && /** @type {{code?: unknown}} */
|
|
1380
|
+
error.code === "ERR_STREAM_PREMATURE_CLOSE";
|
|
1381
|
+
if (!isExpectedEarlyClose) {
|
|
1246
1382
|
throw error;
|
|
1247
1383
|
}
|
|
1248
1384
|
}
|
|
@@ -1255,6 +1391,7 @@ var init_QvdFileReader = __esm({
|
|
|
1255
1391
|
}
|
|
1256
1392
|
);
|
|
1257
1393
|
}
|
|
1394
|
+
headerBuffer = Buffer.concat(headerChunks);
|
|
1258
1395
|
const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
|
|
1259
1396
|
const headerXml = headerBuffer.subarray(0, headerEndIndex).toString();
|
|
1260
1397
|
const headerObj = await xml2.parseStringPromise(headerXml, { explicitArray: false });
|
|
@@ -1271,12 +1408,50 @@ var init_QvdFileReader = __esm({
|
|
|
1271
1408
|
const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1272
1409
|
const rowsToLoad = Math.min(maxRows, totalRows);
|
|
1273
1410
|
validateSymbolTableSizeEarly(symbolTableLength, this._path);
|
|
1411
|
+
for (const [name, value] of [
|
|
1412
|
+
["Offset", symbolTableLength],
|
|
1413
|
+
["RecordByteSize", recordSize],
|
|
1414
|
+
["NoOfRecords", totalRows]
|
|
1415
|
+
]) {
|
|
1416
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
|
1417
|
+
throw new QvdCorruptedError(`Invalid header value: ${name}`, {
|
|
1418
|
+
name,
|
|
1419
|
+
value,
|
|
1420
|
+
file: this._path,
|
|
1421
|
+
stage: "readData"
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1274
1425
|
const indexTableBytesToRead = rowsToLoad * recordSize;
|
|
1275
1426
|
const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
|
|
1276
|
-
const fd = await
|
|
1427
|
+
const fd = await fs.promises.open(this._path, "r");
|
|
1277
1428
|
try {
|
|
1278
|
-
|
|
1279
|
-
|
|
1429
|
+
const { size: fileSize } = await fd.stat();
|
|
1430
|
+
this._fileSize = fileSize;
|
|
1431
|
+
if (totalBytesToRead > fileSize) {
|
|
1432
|
+
throw new QvdCorruptedError("The file is shorter than its header claims.", {
|
|
1433
|
+
file: this._path,
|
|
1434
|
+
fileSize,
|
|
1435
|
+
requiredBytes: totalBytesToRead,
|
|
1436
|
+
stage: "readData"
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
this._buffer = Buffer.alloc(totalBytesToRead);
|
|
1440
|
+
let position = 0;
|
|
1441
|
+
while (position < totalBytesToRead) {
|
|
1442
|
+
const length = Math.min(READ_CHUNK_SIZE, totalBytesToRead - position);
|
|
1443
|
+
const { bytesRead } = await fd.read(this._buffer, position, length, position);
|
|
1444
|
+
if (bytesRead === 0) {
|
|
1445
|
+
throw new QvdCorruptedError("Unexpected end of file while reading QVD data.", {
|
|
1446
|
+
file: this._path,
|
|
1447
|
+
fileSize,
|
|
1448
|
+
bytesRead: position,
|
|
1449
|
+
requiredBytes: totalBytesToRead,
|
|
1450
|
+
stage: "readData"
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
position += bytesRead;
|
|
1454
|
+
}
|
|
1280
1455
|
} finally {
|
|
1281
1456
|
await fd.close();
|
|
1282
1457
|
}
|
|
@@ -1378,7 +1553,10 @@ var init_QvdFileReader = __esm({
|
|
|
1378
1553
|
} else {
|
|
1379
1554
|
symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
|
|
1380
1555
|
}
|
|
1381
|
-
symbolIndex
|
|
1556
|
+
symbolIndex += bias;
|
|
1557
|
+
if (symbolIndex < 0) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1382
1560
|
const fieldName = field["FieldName"];
|
|
1383
1561
|
symbolUsage.get(fieldName)?.add(symbolIndex);
|
|
1384
1562
|
});
|
|
@@ -1483,7 +1661,8 @@ var init_QvdFileReader = __esm({
|
|
|
1483
1661
|
this._indexTableOffset,
|
|
1484
1662
|
this._buffer.length,
|
|
1485
1663
|
rowsToLoad,
|
|
1486
|
-
this._path
|
|
1664
|
+
this._path,
|
|
1665
|
+
this._fileSize
|
|
1487
1666
|
);
|
|
1488
1667
|
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1489
1668
|
for (const field of fields) {
|
|
@@ -1519,14 +1698,33 @@ var init_QvdFileReader = __esm({
|
|
|
1519
1698
|
});
|
|
1520
1699
|
this._indexTable.push(symbolIndices);
|
|
1521
1700
|
}
|
|
1701
|
+
if (this._indexTable.length !== rowsToLoad) {
|
|
1702
|
+
throw new QvdCorruptedError("Index table contains fewer records than expected", {
|
|
1703
|
+
expectedRows: rowsToLoad,
|
|
1704
|
+
actualRows: this._indexTable.length,
|
|
1705
|
+
totalRows,
|
|
1706
|
+
recordSize,
|
|
1707
|
+
file: this._path,
|
|
1708
|
+
stage: "parseIndexTable"
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1522
1711
|
}
|
|
1523
1712
|
/**
|
|
1524
1713
|
* Loads the QVD file into memory and parses it.
|
|
1525
1714
|
*
|
|
1526
1715
|
* @param {number|null} maxRows The maximum number of rows to load. If null, all rows are loaded.
|
|
1716
|
+
* Must be a non-negative integer when given.
|
|
1717
|
+
* @throws {QvdValidationError} If maxRows is neither null nor a non-negative integer.
|
|
1527
1718
|
* @return {Promise<QvdDataFrame>} The loaded QVD file.
|
|
1528
1719
|
*/
|
|
1529
1720
|
async load(maxRows = null) {
|
|
1721
|
+
if (maxRows !== null && (typeof maxRows !== "number" || !Number.isInteger(maxRows) || maxRows < 0)) {
|
|
1722
|
+
throw new QvdValidationError("maxRows must be a non-negative integer, or null to load all rows", {
|
|
1723
|
+
provided: maxRows,
|
|
1724
|
+
type: typeof maxRows,
|
|
1725
|
+
file: this._path
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1530
1728
|
await this._readData(maxRows);
|
|
1531
1729
|
await this._parseHeader();
|
|
1532
1730
|
let symbolsToKeep = null;
|
|
@@ -1561,7 +1759,7 @@ var init_QvdFileReader = __esm({
|
|
|
1561
1759
|
const symbol = this._symbolTable?.[fieldIndex]?.[symbolIndex];
|
|
1562
1760
|
const value = symbol?.toPrimaryValue();
|
|
1563
1761
|
if (typeof value === "string") {
|
|
1564
|
-
if (!isNaN(Number(value))) {
|
|
1762
|
+
if (value.trim() !== "" && !isNaN(Number(value))) {
|
|
1565
1763
|
return Number(value);
|
|
1566
1764
|
}
|
|
1567
1765
|
}
|
|
@@ -1971,7 +2169,11 @@ var init_QvdDataFrame = __esm({
|
|
|
1971
2169
|
*
|
|
1972
2170
|
* @param {string} path The path to the QVD file.
|
|
1973
2171
|
* @param {Object} [options] Optional writing options.
|
|
1974
|
-
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2172
|
+
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2173
|
+
* must be within this directory, with symlinks resolved first, so a link inside it that points
|
|
2174
|
+
* outside it is rejected. Defaults to the current working directory. To permit an entire
|
|
2175
|
+
* volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
|
|
2176
|
+
* back to the working directory rather than removing the restriction.
|
|
1975
2177
|
* @param {Function} [options.onProgress] Optional progress callback function that receives progress updates during write operations.
|
|
1976
2178
|
*/
|
|
1977
2179
|
async toQvd(path3, options = {}) {
|
|
@@ -1987,11 +2189,17 @@ var init_QvdDataFrame = __esm({
|
|
|
1987
2189
|
*
|
|
1988
2190
|
* @param {string} path The path to the QVD file.
|
|
1989
2191
|
* @param {Object} [options] Optional loading options.
|
|
1990
|
-
* @param {number|null} [options.maxRows] The maximum number of rows to load.
|
|
1991
|
-
*
|
|
2192
|
+
* @param {number|null} [options.maxRows] The maximum number of rows to load. Must be a non-negative
|
|
2193
|
+
* integer; if not specified or null, all rows are loaded. Anything else throws a QvdValidationError.
|
|
2194
|
+
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2195
|
+
* must be within this directory, with symlinks resolved first, so a link inside it that points
|
|
2196
|
+
* outside it is rejected. Defaults to the current working directory. To permit an entire
|
|
2197
|
+
* volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
|
|
2198
|
+
* back to the working directory rather than removing the restriction.
|
|
1992
2199
|
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage
|
|
1993
2200
|
* of available memory (or V8 heap limit, whichever is smaller) can be used. Default is 0.3 (30%).
|
|
1994
2201
|
* Increase this (e.g., to 0.5 or 0.7) when running with larger heap sizes via --max-old-space-size.
|
|
2202
|
+
* @throws {QvdValidationError} If options.maxRows is neither null/undefined nor a non-negative integer.
|
|
1995
2203
|
* @return {Promise<QvdDataFrame>} The data frame of the QVD file.
|
|
1996
2204
|
*/
|
|
1997
2205
|
static async fromQvd(path3, options = {}) {
|