qvdjs 0.6.2 → 0.8.0
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/README.md +170 -6
- package/dist/index.cjs +652 -278
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +650 -278
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,8 @@ var fs2 = require('fs');
|
|
|
5
5
|
var crypto = require('crypto');
|
|
6
6
|
var xml2 = require('xml2js');
|
|
7
7
|
var assert = require('assert');
|
|
8
|
+
var os = require('os');
|
|
9
|
+
var v8 = require('v8');
|
|
8
10
|
|
|
9
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
12
|
|
|
@@ -13,6 +15,8 @@ var fs2__default = /*#__PURE__*/_interopDefault(fs2);
|
|
|
13
15
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
14
16
|
var xml2__default = /*#__PURE__*/_interopDefault(xml2);
|
|
15
17
|
var assert__default = /*#__PURE__*/_interopDefault(assert);
|
|
18
|
+
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
19
|
+
var v8__default = /*#__PURE__*/_interopDefault(v8);
|
|
16
20
|
|
|
17
21
|
var __defProp = Object.defineProperty;
|
|
18
22
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -645,6 +649,520 @@ var init_QvdFileWriter = __esm({
|
|
|
645
649
|
}
|
|
646
650
|
});
|
|
647
651
|
|
|
652
|
+
// src/util/bitUtils.js
|
|
653
|
+
function convertBitsToInt32(bits) {
|
|
654
|
+
if (bits.length === 0) {
|
|
655
|
+
return 0;
|
|
656
|
+
}
|
|
657
|
+
return bits.reduce((value, bit, index) => value += bit * Math.pow(2, index), 0);
|
|
658
|
+
}
|
|
659
|
+
var init_bitUtils = __esm({
|
|
660
|
+
"src/util/bitUtils.js"() {
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
function getHeapLimit() {
|
|
664
|
+
return v8__default.default.getHeapStatistics().heap_size_limit;
|
|
665
|
+
}
|
|
666
|
+
function estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
|
|
667
|
+
const FULL_PARSE_OVERHEAD = 6;
|
|
668
|
+
const MINIMAL_OVERHEAD = 0.01;
|
|
669
|
+
if (maxRows === null || maxRows >= totalRows) {
|
|
670
|
+
return symbolTableSize * FULL_PARSE_OVERHEAD;
|
|
671
|
+
}
|
|
672
|
+
const rowPercentage = maxRows / totalRows;
|
|
673
|
+
const symbolPercentage = Math.sqrt(rowPercentage);
|
|
674
|
+
const keptSymbolsMemory = symbolTableSize * symbolPercentage * FULL_PARSE_OVERHEAD;
|
|
675
|
+
const skippedSymbolsMemory = symbolTableSize * (1 - symbolPercentage) * MINIMAL_OVERHEAD;
|
|
676
|
+
return keptSymbolsMemory + skippedSymbolsMemory;
|
|
677
|
+
}
|
|
678
|
+
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.3) {
|
|
679
|
+
const availableMemory = os__default.default.freemem();
|
|
680
|
+
const heapLimit = getHeapLimit();
|
|
681
|
+
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
682
|
+
const effectiveLimit = Math.min(availableMemory, heapLimit);
|
|
683
|
+
const maxAllowedMemory = effectiveLimit * safetyFactor;
|
|
684
|
+
if (estimatedMemory > maxAllowedMemory) {
|
|
685
|
+
const safeSymbolPercentage = maxAllowedMemory / (symbolTableSize * 6);
|
|
686
|
+
const safeRowPercentage = Math.pow(safeSymbolPercentage, 2);
|
|
687
|
+
const recommendedMaxRows = Math.max(1, Math.floor(totalRows * safeRowPercentage));
|
|
688
|
+
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
689
|
+
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
690
|
+
const availableMB = Math.round(maxAllowedMemory / 1024 / 1024);
|
|
691
|
+
const heapLimitMB = Math.round(heapLimit / 1024 / 1024);
|
|
692
|
+
const availableRamMB = Math.round(availableMemory / 1024 / 1024);
|
|
693
|
+
const limitingFactor = heapLimit < availableMemory ? "V8 heap limit" : "available RAM";
|
|
694
|
+
throw new exports.QvdValidationError(
|
|
695
|
+
`Insufficient memory to load file safely. Symbol table: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB (limited by ${limitingFactor}: ${heapLimitMB}MB heap / ${availableRamMB}MB RAM). Try loading fewer rows using the maxRows parameter (recommended: ${recommendedMaxRows.toLocaleString()} rows or less).`,
|
|
696
|
+
{
|
|
697
|
+
file: filePath,
|
|
698
|
+
symbolTableSize,
|
|
699
|
+
symbolTableSizeMB: sizeMB,
|
|
700
|
+
estimatedMemoryMB: estimatedMB,
|
|
701
|
+
availableMemoryMB: availableMB,
|
|
702
|
+
heapLimitMB,
|
|
703
|
+
availableRamMB,
|
|
704
|
+
limitingFactor,
|
|
705
|
+
totalRows,
|
|
706
|
+
maxRows,
|
|
707
|
+
recommendedMaxRows
|
|
708
|
+
}
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows) {
|
|
713
|
+
const heapLimit = getHeapLimit();
|
|
714
|
+
const LARGE_SYMBOL_TABLE_WARNING = heapLimit * 0.125;
|
|
715
|
+
if (symbolTableSize > LARGE_SYMBOL_TABLE_WARNING && (maxRows === null || maxRows >= totalRows)) {
|
|
716
|
+
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
717
|
+
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
718
|
+
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
719
|
+
const warnMB = Math.round(LARGE_SYMBOL_TABLE_WARNING / 1024 / 1024);
|
|
720
|
+
console.warn(
|
|
721
|
+
`\u26A0\uFE0F Large symbol table detected (${sizeMB}MB > ${warnMB}MB threshold). Loading all ${totalRows.toLocaleString()} rows will use ~${estimatedMB}MB RAM. Consider using the maxRows parameter for better performance and lower memory usage.`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
var init_memoryUtils = __esm({
|
|
726
|
+
"src/util/memoryUtils.js"() {
|
|
727
|
+
init_QvdErrors();
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// src/util/validationUtils.js
|
|
732
|
+
function validateSymbolTableSizeEarly(symbolTableLength, filePath) {
|
|
733
|
+
const heapLimit = getHeapLimit();
|
|
734
|
+
const MAX_SYMBOL_TABLE_SIZE = heapLimit * 0.125;
|
|
735
|
+
if (symbolTableLength > MAX_SYMBOL_TABLE_SIZE) {
|
|
736
|
+
const sizeMB = Math.round(symbolTableLength / 1024 / 1024);
|
|
737
|
+
const maxMB = Math.round(MAX_SYMBOL_TABLE_SIZE / 1024 / 1024);
|
|
738
|
+
const heapMB = Math.round(heapLimit / 1024 / 1024);
|
|
739
|
+
throw new exports.QvdValidationError(
|
|
740
|
+
`Symbol table too large (${sizeMB}MB exceeds ${maxMB}MB limit for lazy loading). This QVD file contains extremely high-cardinality fields. Limit scales with heap size (current: ${heapMB}MB, limit: 12.5% = ${maxMB}MB). Consider: (1) loading the full file without maxRows, (2) increasing heap size with --max-old-space-size, or (3) aggregating high-cardinality fields.`,
|
|
741
|
+
{
|
|
742
|
+
file: filePath,
|
|
743
|
+
symbolTableSize: symbolTableLength,
|
|
744
|
+
symbolTableSizeMB: sizeMB,
|
|
745
|
+
maxAllowed: MAX_SYMBOL_TABLE_SIZE,
|
|
746
|
+
maxAllowedMB: maxMB,
|
|
747
|
+
heapLimitMB: heapMB,
|
|
748
|
+
limitPercentage: 12.5
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function validateSymbolTableSize(symbolTableLength, filePath, totalRows) {
|
|
754
|
+
const heapLimit = getHeapLimit();
|
|
755
|
+
const ABSOLUTE_MAX_SYMBOL_TABLE = heapLimit * 0.5;
|
|
756
|
+
if (symbolTableLength > ABSOLUTE_MAX_SYMBOL_TABLE) {
|
|
757
|
+
const sizeMB = Math.round(symbolTableLength / 1024 / 1024);
|
|
758
|
+
const maxMB = Math.round(ABSOLUTE_MAX_SYMBOL_TABLE / 1024 / 1024);
|
|
759
|
+
const heapMB = Math.round(heapLimit / 1024 / 1024);
|
|
760
|
+
throw new exports.QvdValidationError(
|
|
761
|
+
`Symbol table exceeds absolute maximum size (${sizeMB}MB > ${maxMB}MB). This QVD file has pathological cardinality (likely a data modeling issue). Limit scales with heap size (current: ${heapMB}MB, limit: 50% = ${maxMB}MB). Consider: (1) increasing heap size with --max-old-space-size, (2) aggregating high-cardinality fields, (3) splitting the data, or (4) using a different format.`,
|
|
762
|
+
{
|
|
763
|
+
file: filePath,
|
|
764
|
+
symbolTableSize: symbolTableLength,
|
|
765
|
+
symbolTableSizeMB: sizeMB,
|
|
766
|
+
maxAllowed: ABSOLUTE_MAX_SYMBOL_TABLE,
|
|
767
|
+
maxAllowedMB: maxMB,
|
|
768
|
+
heapLimitMB: heapMB,
|
|
769
|
+
limitPercentage: 50,
|
|
770
|
+
totalRows
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function validateFieldMetadata(field, symbolBufferLength, filePath) {
|
|
776
|
+
const symbolsOffset = parseInt(field["Offset"], 10);
|
|
777
|
+
const symbolsLength = parseInt(field["Length"], 10);
|
|
778
|
+
if (isNaN(symbolsOffset) || !Number.isSafeInteger(symbolsOffset) || symbolsOffset < 0) {
|
|
779
|
+
throw new exports.QvdCorruptedError("Invalid symbol offset", {
|
|
780
|
+
field: field["FieldName"],
|
|
781
|
+
offset: symbolsOffset,
|
|
782
|
+
file: filePath,
|
|
783
|
+
stage: "parseSymbolTable"
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
if (isNaN(symbolsLength) || !Number.isSafeInteger(symbolsLength) || symbolsLength < 0) {
|
|
787
|
+
throw new exports.QvdCorruptedError("Invalid symbol length", {
|
|
788
|
+
field: field["FieldName"],
|
|
789
|
+
length: symbolsLength,
|
|
790
|
+
file: filePath,
|
|
791
|
+
stage: "parseSymbolTable"
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
if (symbolsOffset + symbolsLength > symbolBufferLength) {
|
|
795
|
+
throw new exports.QvdCorruptedError("Symbol data extends beyond buffer", {
|
|
796
|
+
field: field["FieldName"],
|
|
797
|
+
offset: symbolsOffset,
|
|
798
|
+
length: symbolsLength,
|
|
799
|
+
bufferSize: symbolBufferLength,
|
|
800
|
+
file: filePath,
|
|
801
|
+
stage: "parseSymbolTable"
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath) {
|
|
806
|
+
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
807
|
+
throw new exports.QvdCorruptedError("Invalid record byte size", {
|
|
808
|
+
recordSize,
|
|
809
|
+
file: filePath,
|
|
810
|
+
stage: "parseIndexTable"
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (isNaN(totalRows) || !Number.isSafeInteger(totalRows) || totalRows < 0) {
|
|
814
|
+
throw new exports.QvdCorruptedError("Invalid number of records", {
|
|
815
|
+
totalRows,
|
|
816
|
+
file: filePath,
|
|
817
|
+
stage: "parseIndexTable"
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
if (recordSize === 0 && totalRows > 0) {
|
|
821
|
+
throw new exports.QvdCorruptedError("Record byte size cannot be zero when records exist", {
|
|
822
|
+
recordSize,
|
|
823
|
+
totalRows,
|
|
824
|
+
file: filePath,
|
|
825
|
+
stage: "parseIndexTable"
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
if (recordSize > 1048576) {
|
|
829
|
+
throw new exports.QvdCorruptedError("Record byte size exceeds maximum", {
|
|
830
|
+
recordSize,
|
|
831
|
+
maxSize: 1048576,
|
|
832
|
+
file: filePath,
|
|
833
|
+
stage: "parseIndexTable"
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
if (isNaN(indexTableLength) || !Number.isSafeInteger(indexTableLength) || indexTableLength < 0) {
|
|
837
|
+
throw new exports.QvdCorruptedError("Invalid index table length", {
|
|
838
|
+
length: indexTableLength,
|
|
839
|
+
file: filePath,
|
|
840
|
+
stage: "parseIndexTable"
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
if (indexTableOffset > bufferLength) {
|
|
844
|
+
throw new exports.QvdCorruptedError("Index table offset beyond buffer", {
|
|
845
|
+
indexTableOffset,
|
|
846
|
+
bufferSize: bufferLength,
|
|
847
|
+
file: filePath,
|
|
848
|
+
stage: "parseIndexTable"
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
const maxReasonableOverage = 100 * 1024 * 1024;
|
|
852
|
+
if (indexTableLength > bufferLength + maxReasonableOverage) {
|
|
853
|
+
throw new exports.QvdCorruptedError("Index table length unreasonably large", {
|
|
854
|
+
indexTableLength,
|
|
855
|
+
bufferSize: bufferLength,
|
|
856
|
+
file: filePath,
|
|
857
|
+
stage: "parseIndexTable"
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
const requiredIndexBytes = rowsToLoad * recordSize;
|
|
861
|
+
if (indexTableLength < requiredIndexBytes) {
|
|
862
|
+
throw new exports.QvdCorruptedError("Index table length smaller than required", {
|
|
863
|
+
indexTableLength,
|
|
864
|
+
requiredBytes: requiredIndexBytes,
|
|
865
|
+
rowsToLoad,
|
|
866
|
+
recordSize,
|
|
867
|
+
file: filePath,
|
|
868
|
+
stage: "parseIndexTable"
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function validateFieldBitMetadata(field, recordSize, filePath) {
|
|
873
|
+
const bitOffset = parseInt(field["BitOffset"], 10);
|
|
874
|
+
const bitWidth = parseInt(field["BitWidth"], 10);
|
|
875
|
+
if (isNaN(bitOffset) || !Number.isSafeInteger(bitOffset) || bitOffset < 0) {
|
|
876
|
+
throw new exports.QvdCorruptedError("Invalid bit offset", {
|
|
877
|
+
field: field["FieldName"],
|
|
878
|
+
bitOffset,
|
|
879
|
+
file: filePath,
|
|
880
|
+
stage: "parseIndexTable"
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
if (isNaN(bitWidth) || !Number.isSafeInteger(bitWidth) || bitWidth < 0) {
|
|
884
|
+
throw new exports.QvdCorruptedError("Invalid bit width", {
|
|
885
|
+
field: field["FieldName"],
|
|
886
|
+
bitWidth,
|
|
887
|
+
file: filePath,
|
|
888
|
+
stage: "parseIndexTable"
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
const recordSizeInBits = recordSize * 8;
|
|
892
|
+
if (bitOffset + bitWidth > recordSizeInBits) {
|
|
893
|
+
throw new exports.QvdCorruptedError("Bit field extends beyond record size", {
|
|
894
|
+
field: field["FieldName"],
|
|
895
|
+
bitOffset,
|
|
896
|
+
bitWidth,
|
|
897
|
+
recordSizeInBits,
|
|
898
|
+
file: filePath,
|
|
899
|
+
stage: "parseIndexTable"
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
var init_validationUtils = __esm({
|
|
904
|
+
"src/util/validationUtils.js"() {
|
|
905
|
+
init_QvdErrors();
|
|
906
|
+
init_memoryUtils();
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// src/util/symbolParser.js
|
|
911
|
+
function parseIntegerSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
912
|
+
if (pointer + 4 > bufferLength) {
|
|
913
|
+
throw new exports.QvdCorruptedError("Buffer overflow reading integer symbol", {
|
|
914
|
+
field: fieldName,
|
|
915
|
+
pointer,
|
|
916
|
+
bufferSize: bufferLength,
|
|
917
|
+
file: filePath,
|
|
918
|
+
stage: "parseSymbolTable"
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
const byteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 4));
|
|
922
|
+
const value = Buffer.from(byteData).readIntLE(0, byteData.length);
|
|
923
|
+
return { symbol: exports.QvdSymbol.fromIntValue(value), bytesRead: 4 };
|
|
924
|
+
}
|
|
925
|
+
function parseDoubleSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
926
|
+
if (pointer + 8 > bufferLength) {
|
|
927
|
+
throw new exports.QvdCorruptedError("Buffer overflow reading double symbol", {
|
|
928
|
+
field: fieldName,
|
|
929
|
+
pointer,
|
|
930
|
+
bufferSize: bufferLength,
|
|
931
|
+
file: filePath,
|
|
932
|
+
stage: "parseSymbolTable"
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
const byteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 8));
|
|
936
|
+
const value = Buffer.from(byteData).readDoubleLE(0);
|
|
937
|
+
return { symbol: exports.QvdSymbol.fromDoubleValue(value), bytesRead: 8 };
|
|
938
|
+
}
|
|
939
|
+
function parseStringSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
940
|
+
const startPointer = pointer;
|
|
941
|
+
const maxStringLength = 1048576;
|
|
942
|
+
let stringLength = 0;
|
|
943
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
944
|
+
if (stringLength >= maxStringLength) {
|
|
945
|
+
throw new exports.QvdCorruptedError("String symbol exceeds maximum length", {
|
|
946
|
+
field: fieldName,
|
|
947
|
+
maxLength: maxStringLength,
|
|
948
|
+
file: filePath,
|
|
949
|
+
stage: "parseSymbolTable"
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
pointer++;
|
|
953
|
+
stringLength++;
|
|
954
|
+
}
|
|
955
|
+
if (pointer >= bufferLength) {
|
|
956
|
+
throw new exports.QvdCorruptedError("String symbol not null-terminated", {
|
|
957
|
+
field: fieldName,
|
|
958
|
+
pointer,
|
|
959
|
+
bufferSize: bufferLength,
|
|
960
|
+
file: filePath,
|
|
961
|
+
stage: "parseSymbolTable"
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const value = symbolBuffer.subarray(startPointer, pointer).toString("utf-8");
|
|
965
|
+
return { symbol: exports.QvdSymbol.fromStringValue(value), bytesRead: pointer - startPointer + 1 };
|
|
966
|
+
}
|
|
967
|
+
function skipStringSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
968
|
+
const startPointer = pointer;
|
|
969
|
+
const maxStringLength = 1048576;
|
|
970
|
+
let stringLength = 0;
|
|
971
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
972
|
+
if (stringLength >= maxStringLength) {
|
|
973
|
+
throw new exports.QvdCorruptedError("String symbol exceeds maximum length", {
|
|
974
|
+
field: fieldName,
|
|
975
|
+
maxLength: maxStringLength,
|
|
976
|
+
file: filePath,
|
|
977
|
+
stage: "parseSymbolTable"
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
pointer++;
|
|
981
|
+
stringLength++;
|
|
982
|
+
}
|
|
983
|
+
if (pointer >= bufferLength) {
|
|
984
|
+
throw new exports.QvdCorruptedError("String symbol not null-terminated", {
|
|
985
|
+
field: fieldName,
|
|
986
|
+
pointer,
|
|
987
|
+
bufferSize: bufferLength,
|
|
988
|
+
file: filePath,
|
|
989
|
+
stage: "parseSymbolTable"
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return pointer - startPointer + 1;
|
|
993
|
+
}
|
|
994
|
+
function parseDualIntegerSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
995
|
+
if (pointer + 4 > bufferLength) {
|
|
996
|
+
throw new exports.QvdCorruptedError("Buffer overflow reading dual integer symbol", {
|
|
997
|
+
field: fieldName,
|
|
998
|
+
pointer,
|
|
999
|
+
bufferSize: bufferLength,
|
|
1000
|
+
file: filePath,
|
|
1001
|
+
stage: "parseSymbolTable"
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
const intByteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 4));
|
|
1005
|
+
const intValue = Buffer.from(intByteData).readIntLE(0, intByteData.length);
|
|
1006
|
+
pointer += 4;
|
|
1007
|
+
const stringStart = pointer;
|
|
1008
|
+
const maxStringLength = 1048576;
|
|
1009
|
+
let stringLength = 0;
|
|
1010
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
1011
|
+
if (stringLength >= maxStringLength) {
|
|
1012
|
+
throw new exports.QvdCorruptedError("Dual string symbol exceeds maximum length", {
|
|
1013
|
+
field: fieldName,
|
|
1014
|
+
maxLength: maxStringLength,
|
|
1015
|
+
file: filePath,
|
|
1016
|
+
stage: "parseSymbolTable"
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
pointer++;
|
|
1020
|
+
stringLength++;
|
|
1021
|
+
}
|
|
1022
|
+
if (pointer >= bufferLength) {
|
|
1023
|
+
throw new exports.QvdCorruptedError("Dual string symbol not null-terminated", {
|
|
1024
|
+
field: fieldName,
|
|
1025
|
+
pointer,
|
|
1026
|
+
bufferSize: bufferLength,
|
|
1027
|
+
file: filePath,
|
|
1028
|
+
stage: "parseSymbolTable"
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
const stringValue = symbolBuffer.subarray(stringStart, pointer).toString("utf-8");
|
|
1032
|
+
return { symbol: exports.QvdSymbol.fromDualIntValue(intValue, stringValue), bytesRead: pointer - (stringStart - 4) + 1 };
|
|
1033
|
+
}
|
|
1034
|
+
function parseDualDoubleSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
1035
|
+
if (pointer + 8 > bufferLength) {
|
|
1036
|
+
throw new exports.QvdCorruptedError("Buffer overflow reading dual double symbol", {
|
|
1037
|
+
field: fieldName,
|
|
1038
|
+
pointer,
|
|
1039
|
+
bufferSize: bufferLength,
|
|
1040
|
+
file: filePath,
|
|
1041
|
+
stage: "parseSymbolTable"
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
const doubleByteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 8));
|
|
1045
|
+
const doubleValue = Buffer.from(doubleByteData).readDoubleLE(0);
|
|
1046
|
+
pointer += 8;
|
|
1047
|
+
const stringStart = pointer;
|
|
1048
|
+
const maxStringLength = 1048576;
|
|
1049
|
+
let stringLength = 0;
|
|
1050
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
1051
|
+
if (stringLength >= maxStringLength) {
|
|
1052
|
+
throw new exports.QvdCorruptedError("Dual string symbol exceeds maximum length", {
|
|
1053
|
+
field: fieldName,
|
|
1054
|
+
maxLength: maxStringLength,
|
|
1055
|
+
file: filePath,
|
|
1056
|
+
stage: "parseSymbolTable"
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
pointer++;
|
|
1060
|
+
stringLength++;
|
|
1061
|
+
}
|
|
1062
|
+
if (pointer >= bufferLength) {
|
|
1063
|
+
throw new exports.QvdCorruptedError("Dual string symbol not null-terminated", {
|
|
1064
|
+
field: fieldName,
|
|
1065
|
+
pointer,
|
|
1066
|
+
bufferSize: bufferLength,
|
|
1067
|
+
file: filePath,
|
|
1068
|
+
stage: "parseSymbolTable"
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
const stringValue = symbolBuffer.subarray(stringStart, pointer).toString("utf-8");
|
|
1072
|
+
return {
|
|
1073
|
+
symbol: exports.QvdSymbol.fromDualDoubleValue(doubleValue, stringValue),
|
|
1074
|
+
bytesRead: pointer - (stringStart - 8) + 1
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function skipDualSymbol(symbolBuffer, pointer, bufferLength, numericBytes, fieldName, filePath) {
|
|
1078
|
+
if (pointer + numericBytes > bufferLength) {
|
|
1079
|
+
throw new exports.QvdCorruptedError(`Buffer overflow reading dual symbol`, {
|
|
1080
|
+
field: fieldName,
|
|
1081
|
+
pointer,
|
|
1082
|
+
bufferSize: bufferLength,
|
|
1083
|
+
file: filePath,
|
|
1084
|
+
stage: "parseSymbolTable"
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const startPointer = pointer;
|
|
1088
|
+
pointer += numericBytes;
|
|
1089
|
+
const maxStringLength = 1048576;
|
|
1090
|
+
let stringLength = 0;
|
|
1091
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
1092
|
+
if (stringLength >= maxStringLength) {
|
|
1093
|
+
throw new exports.QvdCorruptedError("Dual string symbol exceeds maximum length", {
|
|
1094
|
+
field: fieldName,
|
|
1095
|
+
maxLength: maxStringLength,
|
|
1096
|
+
file: filePath,
|
|
1097
|
+
stage: "parseSymbolTable"
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
pointer++;
|
|
1101
|
+
stringLength++;
|
|
1102
|
+
}
|
|
1103
|
+
if (pointer >= bufferLength) {
|
|
1104
|
+
throw new exports.QvdCorruptedError("Dual string symbol not null-terminated", {
|
|
1105
|
+
field: fieldName,
|
|
1106
|
+
pointer,
|
|
1107
|
+
bufferSize: bufferLength,
|
|
1108
|
+
file: filePath,
|
|
1109
|
+
stage: "parseSymbolTable"
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
return pointer - startPointer + 1;
|
|
1113
|
+
}
|
|
1114
|
+
function parseSymbol(typeByte, symbolBuffer, pointer, bufferLength, fieldName, filePath, shouldParse) {
|
|
1115
|
+
switch (typeByte) {
|
|
1116
|
+
case 1: {
|
|
1117
|
+
if (shouldParse) {
|
|
1118
|
+
return parseIntegerSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1119
|
+
}
|
|
1120
|
+
return { symbol: null, bytesRead: 4 };
|
|
1121
|
+
}
|
|
1122
|
+
case 2: {
|
|
1123
|
+
if (shouldParse) {
|
|
1124
|
+
return parseDoubleSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1125
|
+
}
|
|
1126
|
+
return { symbol: null, bytesRead: 8 };
|
|
1127
|
+
}
|
|
1128
|
+
case 4: {
|
|
1129
|
+
if (shouldParse) {
|
|
1130
|
+
return parseStringSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1131
|
+
}
|
|
1132
|
+
const bytesRead = skipStringSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1133
|
+
return { symbol: null, bytesRead };
|
|
1134
|
+
}
|
|
1135
|
+
case 5: {
|
|
1136
|
+
if (shouldParse) {
|
|
1137
|
+
return parseDualIntegerSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1138
|
+
}
|
|
1139
|
+
const bytesRead = skipDualSymbol(symbolBuffer, pointer, bufferLength, 4, fieldName, filePath);
|
|
1140
|
+
return { symbol: null, bytesRead };
|
|
1141
|
+
}
|
|
1142
|
+
case 6: {
|
|
1143
|
+
if (shouldParse) {
|
|
1144
|
+
return parseDualDoubleSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1145
|
+
}
|
|
1146
|
+
const bytesRead = skipDualSymbol(symbolBuffer, pointer, bufferLength, 8, fieldName, filePath);
|
|
1147
|
+
return { symbol: null, bytesRead };
|
|
1148
|
+
}
|
|
1149
|
+
default: {
|
|
1150
|
+
throw new exports.QvdParseError("Unknown symbol type byte", {
|
|
1151
|
+
typeByte: typeByte.toString(16),
|
|
1152
|
+
offset: pointer - 1,
|
|
1153
|
+
file: filePath,
|
|
1154
|
+
stage: "parseSymbolTable"
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
var init_symbolParser = __esm({
|
|
1160
|
+
"src/util/symbolParser.js"() {
|
|
1161
|
+
init_QvdSymbol();
|
|
1162
|
+
init_QvdErrors();
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
|
|
648
1166
|
// src/QvdFileReader.js
|
|
649
1167
|
var QvdFileReader_exports = {};
|
|
650
1168
|
__export(QvdFileReader_exports, {
|
|
@@ -653,10 +1171,13 @@ __export(QvdFileReader_exports, {
|
|
|
653
1171
|
exports.QvdFileReader = void 0;
|
|
654
1172
|
var init_QvdFileReader = __esm({
|
|
655
1173
|
"src/QvdFileReader.js"() {
|
|
656
|
-
init_QvdSymbol();
|
|
657
1174
|
init_QvdDataFrame();
|
|
658
1175
|
init_QvdErrors();
|
|
659
1176
|
init_validatePath();
|
|
1177
|
+
init_bitUtils();
|
|
1178
|
+
init_memoryUtils();
|
|
1179
|
+
init_validationUtils();
|
|
1180
|
+
init_symbolParser();
|
|
660
1181
|
exports.QvdFileReader = class {
|
|
661
1182
|
/**
|
|
662
1183
|
* Constructs a new QVD file parser.
|
|
@@ -665,10 +1186,14 @@ var init_QvdFileReader = __esm({
|
|
|
665
1186
|
* @param {Object} [options={}] Options for the reader.
|
|
666
1187
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
667
1188
|
* path must be within this directory. Defaults to current working directory.
|
|
1189
|
+
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
|
|
1190
|
+
* what percentage of available memory (or V8 heap limit, whichever is smaller) can be used
|
|
1191
|
+
* for loading QVD files. Default is 0.3 (30%). Increase for larger heap configurations.
|
|
668
1192
|
*/
|
|
669
1193
|
constructor(filePath, options = {}) {
|
|
670
|
-
const { allowedDir } = options;
|
|
1194
|
+
const { allowedDir, memorySafetyFactor = 0.3 } = options;
|
|
671
1195
|
this._path = validatePath(filePath, allowedDir);
|
|
1196
|
+
this._memorySafetyFactor = memorySafetyFactor;
|
|
672
1197
|
this._buffer = null;
|
|
673
1198
|
this._headerOffset = null;
|
|
674
1199
|
this._symbolTableOffset = null;
|
|
@@ -754,6 +1279,7 @@ var init_QvdFileReader = __esm({
|
|
|
754
1279
|
const recordSize = parseInt(headerObj["QvdTableHeader"]["RecordByteSize"], 10);
|
|
755
1280
|
const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
|
|
756
1281
|
const rowsToLoad = Math.min(maxRows, totalRows);
|
|
1282
|
+
validateSymbolTableSizeEarly(symbolTableLength, this._path);
|
|
757
1283
|
const indexTableBytesToRead = rowsToLoad * recordSize;
|
|
758
1284
|
const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
|
|
759
1285
|
const fd = await fs2__default.default.promises.open(this._path, "r");
|
|
@@ -803,11 +1329,80 @@ var init_QvdFileReader = __esm({
|
|
|
803
1329
|
this._symbolTableOffset = headerEndIndex;
|
|
804
1330
|
this._indexTableOffset = this._symbolTableOffset + parseInt(this._header["QvdTableHeader"]["Offset"], 10);
|
|
805
1331
|
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Estimates the memory usage for loading a QVD file with given parameters.
|
|
1334
|
+
* Takes into account Phase 2.5 optimization which skips parsing unused symbols.
|
|
1335
|
+
*
|
|
1336
|
+
* @param {number} symbolTableSize Size of symbol table in bytes
|
|
1337
|
+
* @param {number|null} maxRows Maximum number of rows to load (null = all rows)
|
|
1338
|
+
* @param {number} totalRows Total number of rows in the file
|
|
1339
|
+
* @returns {number} Estimated memory usage in bytes
|
|
1340
|
+
* @private
|
|
1341
|
+
*/
|
|
1342
|
+
_estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
|
|
1343
|
+
return estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Analyzes the index table to determine which symbols are actually needed.
|
|
1347
|
+
* This is used for two-pass symbol filtering optimization.
|
|
1348
|
+
*
|
|
1349
|
+
* @param {number} maxRows The maximum number of rows to analyze.
|
|
1350
|
+
* @returns {Promise<Map<string, Set<number>>>} Map of field names to Set of needed symbol indices.
|
|
1351
|
+
* @private
|
|
1352
|
+
*/
|
|
1353
|
+
async _analyzeIndexTableSymbolUsage(maxRows) {
|
|
1354
|
+
if (!this._buffer || !this._header || !this._indexTableOffset) {
|
|
1355
|
+
throw new exports.QvdCorruptedError(
|
|
1356
|
+
"The QVD file has not been loaded in the proper order or has not been loaded at all.",
|
|
1357
|
+
{
|
|
1358
|
+
file: this._path,
|
|
1359
|
+
stage: "analyzeIndexTableSymbolUsage"
|
|
1360
|
+
}
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
let fields = this._header["QvdTableHeader"]["Fields"]["QvdFieldHeader"];
|
|
1364
|
+
if (!Array.isArray(fields)) {
|
|
1365
|
+
fields = [fields];
|
|
1366
|
+
}
|
|
1367
|
+
const recordSize = parseInt(this._header["QvdTableHeader"]["RecordByteSize"], 10);
|
|
1368
|
+
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1369
|
+
const rowsToAnalyze = Math.min(maxRows, totalRows);
|
|
1370
|
+
const indexTableLength = parseInt(this._header["QvdTableHeader"]["Length"], 10);
|
|
1371
|
+
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1372
|
+
const symbolUsage = /* @__PURE__ */ new Map();
|
|
1373
|
+
fields.forEach((field) => {
|
|
1374
|
+
symbolUsage.set(field["FieldName"], /* @__PURE__ */ new Set());
|
|
1375
|
+
});
|
|
1376
|
+
for (let pointer = 0, rowCount = 0; pointer < indexBuffer.length && rowCount < rowsToAnalyze; pointer += recordSize, rowCount++) {
|
|
1377
|
+
const bytes = new Int32Array(indexBuffer.subarray(pointer, pointer + recordSize));
|
|
1378
|
+
bytes.reverse();
|
|
1379
|
+
const mask = bytes.reduce((bits, byte) => bits + ("00000000" + byte.toString(2)).slice(-8), "").split("").reverse().map((bit) => parseInt(bit));
|
|
1380
|
+
fields.forEach((field) => {
|
|
1381
|
+
const bitOffset = parseInt(field["BitOffset"], 10);
|
|
1382
|
+
const bitWidth = parseInt(field["BitWidth"], 10);
|
|
1383
|
+
const bias = parseInt(field["Bias"], 10);
|
|
1384
|
+
let symbolIndex;
|
|
1385
|
+
if (bitWidth === 0) {
|
|
1386
|
+
symbolIndex = 0;
|
|
1387
|
+
} else {
|
|
1388
|
+
symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
|
|
1389
|
+
}
|
|
1390
|
+
symbolIndex -= bias;
|
|
1391
|
+
const fieldName = field["FieldName"];
|
|
1392
|
+
symbolUsage.get(fieldName)?.add(symbolIndex);
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
return symbolUsage;
|
|
1396
|
+
}
|
|
806
1397
|
/**
|
|
807
1398
|
* Parses the symbol table of the QVD file. This method is part of the parsing process
|
|
808
1399
|
* and should not be called directly.
|
|
1400
|
+
*
|
|
1401
|
+
* @param {Map<string, Set<number>>|null} symbolsToKeep Optional map of field names to symbol indices to keep.
|
|
1402
|
+
* If provided, only these symbols will be parsed (two-pass filtering optimization).
|
|
1403
|
+
* @param {number|null} maxRows Optional maximum number of rows being loaded (for memory estimation).
|
|
809
1404
|
*/
|
|
810
|
-
async _parseSymbolTable() {
|
|
1405
|
+
async _parseSymbolTable(symbolsToKeep = null, maxRows = null) {
|
|
811
1406
|
if (!this._buffer || !this._header || !this._symbolTableOffset || !this._indexTableOffset) {
|
|
812
1407
|
throw new exports.QvdCorruptedError(
|
|
813
1408
|
"The QVD file has not been loaded in the proper order or has not been loaded at all.",
|
|
@@ -819,192 +1414,40 @@ var init_QvdFileReader = __esm({
|
|
|
819
1414
|
}
|
|
820
1415
|
let fields = this._header["QvdTableHeader"]["Fields"]["QvdFieldHeader"];
|
|
821
1416
|
const symbolBuffer = this._buffer.subarray(this._symbolTableOffset, this._indexTableOffset);
|
|
1417
|
+
const symbolTableSize = symbolBuffer.length;
|
|
1418
|
+
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1419
|
+
validateSymbolTableSize(symbolTableSize, this._path, totalRows);
|
|
1420
|
+
validateMemoryAvailability(symbolTableSize, maxRows, totalRows, this._path, this._memorySafetyFactor);
|
|
1421
|
+
warnLargeSymbolTable(symbolTableSize, maxRows, totalRows);
|
|
822
1422
|
if (!Array.isArray(fields)) {
|
|
823
1423
|
fields = [fields];
|
|
824
1424
|
}
|
|
825
1425
|
for (const field of fields) {
|
|
826
|
-
|
|
827
|
-
const symbolsLength = parseInt(field["Length"], 10);
|
|
828
|
-
if (isNaN(symbolsOffset) || !Number.isSafeInteger(symbolsOffset) || symbolsOffset < 0) {
|
|
829
|
-
throw new exports.QvdCorruptedError("Invalid symbol offset", {
|
|
830
|
-
field: field["FieldName"],
|
|
831
|
-
offset: symbolsOffset,
|
|
832
|
-
file: this._path,
|
|
833
|
-
stage: "parseSymbolTable"
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
if (isNaN(symbolsLength) || !Number.isSafeInteger(symbolsLength) || symbolsLength < 0) {
|
|
837
|
-
throw new exports.QvdCorruptedError("Invalid symbol length", {
|
|
838
|
-
field: field["FieldName"],
|
|
839
|
-
length: symbolsLength,
|
|
840
|
-
file: this._path,
|
|
841
|
-
stage: "parseSymbolTable"
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
if (symbolsOffset + symbolsLength > symbolBuffer.length) {
|
|
845
|
-
throw new exports.QvdCorruptedError("Symbol data extends beyond buffer", {
|
|
846
|
-
field: field["FieldName"],
|
|
847
|
-
offset: symbolsOffset,
|
|
848
|
-
length: symbolsLength,
|
|
849
|
-
bufferSize: symbolBuffer.length,
|
|
850
|
-
file: this._path,
|
|
851
|
-
stage: "parseSymbolTable"
|
|
852
|
-
});
|
|
853
|
-
}
|
|
1426
|
+
validateFieldMetadata(field, symbolBuffer.length, this._path);
|
|
854
1427
|
}
|
|
855
1428
|
this._symbolTable = fields.map((field) => {
|
|
856
1429
|
const symbolsOffset = parseInt(field["Offset"], 10);
|
|
857
1430
|
const symbolsLength = parseInt(field["Length"], 10);
|
|
1431
|
+
const fieldName = field["FieldName"];
|
|
1432
|
+
const neededSymbols = symbolsToKeep ? symbolsToKeep.get(fieldName) : null;
|
|
1433
|
+
const filteringEnabled = neededSymbols !== null;
|
|
858
1434
|
const symbols = [];
|
|
1435
|
+
let symbolIndex = 0;
|
|
859
1436
|
for (let pointer = symbolsOffset; pointer < symbolsOffset + symbolsLength; pointer++) {
|
|
860
1437
|
const typeByte = symbolBuffer[pointer++];
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
pointer += 3;
|
|
875
|
-
symbols.push(exports.QvdSymbol.fromIntValue(value));
|
|
876
|
-
break;
|
|
877
|
-
}
|
|
878
|
-
case 2: {
|
|
879
|
-
if (pointer + 8 > symbolBuffer.length) {
|
|
880
|
-
throw new exports.QvdCorruptedError("Buffer overflow reading double symbol", {
|
|
881
|
-
field: field["FieldName"],
|
|
882
|
-
pointer,
|
|
883
|
-
bufferSize: symbolBuffer.length,
|
|
884
|
-
file: this._path,
|
|
885
|
-
stage: "parseSymbolTable"
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
const byteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 8));
|
|
889
|
-
const value = Buffer.from(byteData).readDoubleLE(0);
|
|
890
|
-
pointer += 7;
|
|
891
|
-
symbols.push(exports.QvdSymbol.fromDoubleValue(value));
|
|
892
|
-
break;
|
|
893
|
-
}
|
|
894
|
-
case 4: {
|
|
895
|
-
const byteData = [];
|
|
896
|
-
const maxStringLength = 1048576;
|
|
897
|
-
while (pointer < symbolBuffer.length && symbolBuffer[pointer] !== 0) {
|
|
898
|
-
if (byteData.length >= maxStringLength) {
|
|
899
|
-
throw new exports.QvdCorruptedError("String symbol exceeds maximum length", {
|
|
900
|
-
field: field["FieldName"],
|
|
901
|
-
maxLength: maxStringLength,
|
|
902
|
-
file: this._path,
|
|
903
|
-
stage: "parseSymbolTable"
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
byteData.push(symbolBuffer[pointer++]);
|
|
907
|
-
}
|
|
908
|
-
if (pointer >= symbolBuffer.length) {
|
|
909
|
-
throw new exports.QvdCorruptedError("String symbol not null-terminated", {
|
|
910
|
-
field: field["FieldName"],
|
|
911
|
-
pointer,
|
|
912
|
-
bufferSize: symbolBuffer.length,
|
|
913
|
-
file: this._path,
|
|
914
|
-
stage: "parseSymbolTable"
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
const value = Buffer.from(byteData).toString("utf-8");
|
|
918
|
-
symbols.push(exports.QvdSymbol.fromStringValue(value));
|
|
919
|
-
break;
|
|
920
|
-
}
|
|
921
|
-
case 5: {
|
|
922
|
-
if (pointer + 4 > symbolBuffer.length) {
|
|
923
|
-
throw new exports.QvdCorruptedError("Buffer overflow reading dual integer symbol", {
|
|
924
|
-
field: field["FieldName"],
|
|
925
|
-
pointer,
|
|
926
|
-
bufferSize: symbolBuffer.length,
|
|
927
|
-
file: this._path,
|
|
928
|
-
stage: "parseSymbolTable"
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
const intByteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 4));
|
|
932
|
-
const intValue = Buffer.from(intByteData).readIntLE(0, intByteData.length);
|
|
933
|
-
pointer += 4;
|
|
934
|
-
const stringByteData = [];
|
|
935
|
-
const maxStringLength = 1048576;
|
|
936
|
-
while (pointer < symbolBuffer.length && symbolBuffer[pointer] !== 0) {
|
|
937
|
-
if (stringByteData.length >= maxStringLength) {
|
|
938
|
-
throw new exports.QvdCorruptedError("Dual string symbol exceeds maximum length", {
|
|
939
|
-
field: field["FieldName"],
|
|
940
|
-
maxLength: maxStringLength,
|
|
941
|
-
file: this._path,
|
|
942
|
-
stage: "parseSymbolTable"
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
stringByteData.push(symbolBuffer[pointer++]);
|
|
946
|
-
}
|
|
947
|
-
if (pointer >= symbolBuffer.length) {
|
|
948
|
-
throw new exports.QvdCorruptedError("Dual string symbol not null-terminated", {
|
|
949
|
-
field: field["FieldName"],
|
|
950
|
-
pointer,
|
|
951
|
-
bufferSize: symbolBuffer.length,
|
|
952
|
-
file: this._path,
|
|
953
|
-
stage: "parseSymbolTable"
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
const stringValue = Buffer.from(stringByteData).toString("utf-8");
|
|
957
|
-
symbols.push(exports.QvdSymbol.fromDualIntValue(intValue, stringValue));
|
|
958
|
-
break;
|
|
959
|
-
}
|
|
960
|
-
case 6: {
|
|
961
|
-
if (pointer + 8 > symbolBuffer.length) {
|
|
962
|
-
throw new exports.QvdCorruptedError("Buffer overflow reading dual double symbol", {
|
|
963
|
-
field: field["FieldName"],
|
|
964
|
-
pointer,
|
|
965
|
-
bufferSize: symbolBuffer.length,
|
|
966
|
-
file: this._path,
|
|
967
|
-
stage: "parseSymbolTable"
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
const doubleByteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 8));
|
|
971
|
-
const doubleValue = Buffer.from(doubleByteData).readDoubleLE(0);
|
|
972
|
-
pointer += 8;
|
|
973
|
-
const stringByteData = [];
|
|
974
|
-
const maxStringLength = 1048576;
|
|
975
|
-
while (pointer < symbolBuffer.length && symbolBuffer[pointer] !== 0) {
|
|
976
|
-
if (stringByteData.length >= maxStringLength) {
|
|
977
|
-
throw new exports.QvdCorruptedError("Dual string symbol exceeds maximum length", {
|
|
978
|
-
field: field["FieldName"],
|
|
979
|
-
maxLength: maxStringLength,
|
|
980
|
-
file: this._path,
|
|
981
|
-
stage: "parseSymbolTable"
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
stringByteData.push(symbolBuffer[pointer++]);
|
|
985
|
-
}
|
|
986
|
-
if (pointer >= symbolBuffer.length) {
|
|
987
|
-
throw new exports.QvdCorruptedError("Dual string symbol not null-terminated", {
|
|
988
|
-
field: field["FieldName"],
|
|
989
|
-
pointer,
|
|
990
|
-
bufferSize: symbolBuffer.length,
|
|
991
|
-
file: this._path,
|
|
992
|
-
stage: "parseSymbolTable"
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
const stringValue = Buffer.from(stringByteData).toString("utf-8");
|
|
996
|
-
symbols.push(exports.QvdSymbol.fromDualDoubleValue(doubleValue, stringValue));
|
|
997
|
-
break;
|
|
998
|
-
}
|
|
999
|
-
default: {
|
|
1000
|
-
throw new exports.QvdParseError("Unknown symbol type byte", {
|
|
1001
|
-
typeByte: typeByte.toString(16),
|
|
1002
|
-
offset: pointer,
|
|
1003
|
-
file: this._path,
|
|
1004
|
-
stage: "parseSymbolTable"
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1438
|
+
const shouldKeepSymbol = !filteringEnabled || !!(neededSymbols && neededSymbols.has(symbolIndex));
|
|
1439
|
+
const { symbol, bytesRead } = parseSymbol(
|
|
1440
|
+
typeByte,
|
|
1441
|
+
symbolBuffer,
|
|
1442
|
+
pointer,
|
|
1443
|
+
symbolBuffer.length,
|
|
1444
|
+
fieldName,
|
|
1445
|
+
this._path,
|
|
1446
|
+
shouldKeepSymbol
|
|
1447
|
+
);
|
|
1448
|
+
symbols.push(symbol);
|
|
1449
|
+
pointer += bytesRead - 1;
|
|
1450
|
+
symbolIndex++;
|
|
1008
1451
|
}
|
|
1009
1452
|
return symbols;
|
|
1010
1453
|
});
|
|
@@ -1016,10 +1459,7 @@ var init_QvdFileReader = __esm({
|
|
|
1016
1459
|
* @return {Number} The integer value
|
|
1017
1460
|
*/
|
|
1018
1461
|
_convertBitsToInt32(bits) {
|
|
1019
|
-
|
|
1020
|
-
return 0;
|
|
1021
|
-
}
|
|
1022
|
-
return bits.reduce((value, bit, index) => value += bit * Math.pow(2, index), 0);
|
|
1462
|
+
return convertBitsToInt32(bits);
|
|
1023
1463
|
}
|
|
1024
1464
|
/**
|
|
1025
1465
|
* Parses the bit stuffed index table of the QVD file. This method is part of the parsing process
|
|
@@ -1042,105 +1482,21 @@ var init_QvdFileReader = __esm({
|
|
|
1042
1482
|
fields = [fields];
|
|
1043
1483
|
}
|
|
1044
1484
|
const recordSize = parseInt(this._header["QvdTableHeader"]["RecordByteSize"], 10);
|
|
1045
|
-
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
1046
|
-
throw new exports.QvdCorruptedError("Invalid record byte size", {
|
|
1047
|
-
recordSize,
|
|
1048
|
-
file: this._path,
|
|
1049
|
-
stage: "parseIndexTable"
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
1485
|
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1053
|
-
if (isNaN(totalRows) || !Number.isSafeInteger(totalRows) || totalRows < 0) {
|
|
1054
|
-
throw new exports.QvdCorruptedError("Invalid number of records", {
|
|
1055
|
-
totalRows,
|
|
1056
|
-
file: this._path,
|
|
1057
|
-
stage: "parseIndexTable"
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
if (recordSize === 0 && totalRows > 0) {
|
|
1061
|
-
throw new exports.QvdCorruptedError("Record byte size cannot be zero when records exist", {
|
|
1062
|
-
recordSize,
|
|
1063
|
-
totalRows,
|
|
1064
|
-
file: this._path,
|
|
1065
|
-
stage: "parseIndexTable"
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
if (recordSize > 1048576) {
|
|
1069
|
-
throw new exports.QvdCorruptedError("Record byte size exceeds maximum", {
|
|
1070
|
-
recordSize,
|
|
1071
|
-
maxSize: 1048576,
|
|
1072
|
-
file: this._path,
|
|
1073
|
-
stage: "parseIndexTable"
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
1486
|
const rowsToLoad = maxRows !== null ? Math.min(maxRows, totalRows) : totalRows;
|
|
1077
1487
|
const indexTableLength = parseInt(this._header["QvdTableHeader"]["Length"], 10);
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
indexTableOffset: this._indexTableOffset,
|
|
1088
|
-
bufferSize: this._buffer.length,
|
|
1089
|
-
file: this._path,
|
|
1090
|
-
stage: "parseIndexTable"
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
const maxReasonableOverage = 100 * 1024 * 1024;
|
|
1094
|
-
if (indexTableLength > this._buffer.length + maxReasonableOverage) {
|
|
1095
|
-
throw new exports.QvdCorruptedError("Index table length unreasonably large", {
|
|
1096
|
-
indexTableLength,
|
|
1097
|
-
bufferSize: this._buffer.length,
|
|
1098
|
-
file: this._path,
|
|
1099
|
-
stage: "parseIndexTable"
|
|
1100
|
-
});
|
|
1101
|
-
}
|
|
1102
|
-
const requiredIndexBytes = rowsToLoad * recordSize;
|
|
1103
|
-
if (indexTableLength < requiredIndexBytes) {
|
|
1104
|
-
throw new exports.QvdCorruptedError("Index table length smaller than required", {
|
|
1105
|
-
indexTableLength,
|
|
1106
|
-
requiredBytes: requiredIndexBytes,
|
|
1107
|
-
rowsToLoad,
|
|
1108
|
-
recordSize,
|
|
1109
|
-
file: this._path,
|
|
1110
|
-
stage: "parseIndexTable"
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1488
|
+
validateIndexTableMetadata(
|
|
1489
|
+
recordSize,
|
|
1490
|
+
totalRows,
|
|
1491
|
+
indexTableLength,
|
|
1492
|
+
this._indexTableOffset,
|
|
1493
|
+
this._buffer.length,
|
|
1494
|
+
rowsToLoad,
|
|
1495
|
+
this._path
|
|
1496
|
+
);
|
|
1113
1497
|
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1114
1498
|
for (const field of fields) {
|
|
1115
|
-
|
|
1116
|
-
const bitWidth = parseInt(field["BitWidth"], 10);
|
|
1117
|
-
if (isNaN(bitOffset) || !Number.isSafeInteger(bitOffset) || bitOffset < 0) {
|
|
1118
|
-
throw new exports.QvdCorruptedError("Invalid bit offset", {
|
|
1119
|
-
field: field["FieldName"],
|
|
1120
|
-
bitOffset,
|
|
1121
|
-
file: this._path,
|
|
1122
|
-
stage: "parseIndexTable"
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
if (isNaN(bitWidth) || !Number.isSafeInteger(bitWidth) || bitWidth < 0) {
|
|
1126
|
-
throw new exports.QvdCorruptedError("Invalid bit width", {
|
|
1127
|
-
field: field["FieldName"],
|
|
1128
|
-
bitWidth,
|
|
1129
|
-
file: this._path,
|
|
1130
|
-
stage: "parseIndexTable"
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
const recordSizeInBits = recordSize * 8;
|
|
1134
|
-
if (bitOffset + bitWidth > recordSizeInBits) {
|
|
1135
|
-
throw new exports.QvdCorruptedError("Bit field extends beyond record size", {
|
|
1136
|
-
field: field["FieldName"],
|
|
1137
|
-
bitOffset,
|
|
1138
|
-
bitWidth,
|
|
1139
|
-
recordSizeInBits,
|
|
1140
|
-
file: this._path,
|
|
1141
|
-
stage: "parseIndexTable"
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1499
|
+
validateFieldBitMetadata(field, recordSize, this._path);
|
|
1144
1500
|
}
|
|
1145
1501
|
this._indexTable = [];
|
|
1146
1502
|
for (let pointer = 0, rowCount = 0; pointer < indexBuffer.length && rowCount < rowsToLoad; pointer += recordSize, rowCount++) {
|
|
@@ -1182,7 +1538,19 @@ var init_QvdFileReader = __esm({
|
|
|
1182
1538
|
async load(maxRows = null) {
|
|
1183
1539
|
await this._readData(maxRows);
|
|
1184
1540
|
await this._parseHeader();
|
|
1185
|
-
|
|
1541
|
+
let symbolsToKeep = null;
|
|
1542
|
+
if (maxRows !== null && this._header) {
|
|
1543
|
+
const symbolTableLength = parseInt(this._header["QvdTableHeader"]["Offset"], 10);
|
|
1544
|
+
const SYMBOL_FILTERING_THRESHOLD = 50 * 1024 * 1024;
|
|
1545
|
+
if (symbolTableLength > SYMBOL_FILTERING_THRESHOLD) {
|
|
1546
|
+
symbolsToKeep = await this._analyzeIndexTableSymbolUsage(maxRows);
|
|
1547
|
+
const totalSymbols = Array.from(symbolsToKeep.values()).reduce((sum, set) => sum + set.size, 0);
|
|
1548
|
+
console.log(
|
|
1549
|
+
`[Phase 2.5 Optimization] Using stream-and-skip parsing: keeping ${totalSymbols} symbols from ${(symbolTableLength / 1024 / 1024).toFixed(1)}MB symbol table`
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
await this._parseSymbolTable(symbolsToKeep, maxRows);
|
|
1186
1554
|
await this._parseIndexTable(maxRows);
|
|
1187
1555
|
assert__default.default(this._header, "The QVD file header has not been parsed.");
|
|
1188
1556
|
assert__default.default(this._symbolTable, "The QVD file symbol table has not been parsed.");
|
|
@@ -1630,11 +1998,17 @@ var init_QvdDataFrame = __esm({
|
|
|
1630
1998
|
* @param {Object} [options] Optional loading options.
|
|
1631
1999
|
* @param {number|null} [options.maxRows] The maximum number of rows to load. If not specified, all rows are loaded.
|
|
1632
2000
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path must be within this directory.
|
|
2001
|
+
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage
|
|
2002
|
+
* of available memory (or V8 heap limit, whichever is smaller) can be used. Default is 0.3 (30%).
|
|
2003
|
+
* Increase this (e.g., to 0.5 or 0.7) when running with larger heap sizes via --max-old-space-size.
|
|
1633
2004
|
* @return {Promise<QvdDataFrame>} The data frame of the QVD file.
|
|
1634
2005
|
*/
|
|
1635
2006
|
static async fromQvd(path3, options = {}) {
|
|
1636
2007
|
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
1637
|
-
const readerOptions = {
|
|
2008
|
+
const readerOptions = {
|
|
2009
|
+
allowedDir: options.allowedDir,
|
|
2010
|
+
memorySafetyFactor: options.memorySafetyFactor
|
|
2011
|
+
};
|
|
1638
2012
|
return await new QvdFileReader2(path3, readerOptions).load(options.maxRows !== void 0 ? options.maxRows : null);
|
|
1639
2013
|
}
|
|
1640
2014
|
/**
|