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