qvdjs 0.6.2 → 0.7.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 +121 -6
- package/dist/index.cjs +617 -276
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +616 -276
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ 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';
|
|
6
7
|
|
|
7
8
|
var __defProp = Object.defineProperty;
|
|
8
9
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -635,6 +636,499 @@ var init_QvdFileWriter = __esm({
|
|
|
635
636
|
}
|
|
636
637
|
});
|
|
637
638
|
|
|
639
|
+
// src/util/bitUtils.js
|
|
640
|
+
function convertBitsToInt32(bits) {
|
|
641
|
+
if (bits.length === 0) {
|
|
642
|
+
return 0;
|
|
643
|
+
}
|
|
644
|
+
return bits.reduce((value, bit, index) => value += bit * Math.pow(2, index), 0);
|
|
645
|
+
}
|
|
646
|
+
var init_bitUtils = __esm({
|
|
647
|
+
"src/util/bitUtils.js"() {
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
function estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
|
|
651
|
+
const FULL_PARSE_OVERHEAD = 6;
|
|
652
|
+
const MINIMAL_OVERHEAD = 0.01;
|
|
653
|
+
if (maxRows === null || maxRows >= totalRows) {
|
|
654
|
+
return symbolTableSize * FULL_PARSE_OVERHEAD;
|
|
655
|
+
}
|
|
656
|
+
const rowPercentage = maxRows / totalRows;
|
|
657
|
+
const symbolPercentage = Math.sqrt(rowPercentage);
|
|
658
|
+
const keptSymbolsMemory = symbolTableSize * symbolPercentage * FULL_PARSE_OVERHEAD;
|
|
659
|
+
const skippedSymbolsMemory = symbolTableSize * (1 - symbolPercentage) * MINIMAL_OVERHEAD;
|
|
660
|
+
return keptSymbolsMemory + skippedSymbolsMemory;
|
|
661
|
+
}
|
|
662
|
+
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath) {
|
|
663
|
+
const availableMemory = os.freemem();
|
|
664
|
+
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
665
|
+
const MEMORY_SAFETY_FACTOR = 0.3;
|
|
666
|
+
const maxAllowedMemory = availableMemory * MEMORY_SAFETY_FACTOR;
|
|
667
|
+
if (estimatedMemory > maxAllowedMemory) {
|
|
668
|
+
const safeSymbolPercentage = maxAllowedMemory / (symbolTableSize * 6);
|
|
669
|
+
const safeRowPercentage = Math.pow(safeSymbolPercentage, 2);
|
|
670
|
+
const recommendedMaxRows = Math.max(1, Math.floor(totalRows * safeRowPercentage));
|
|
671
|
+
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
672
|
+
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
673
|
+
const availableMB = Math.round(maxAllowedMemory / 1024 / 1024);
|
|
674
|
+
throw new QvdValidationError(
|
|
675
|
+
`Insufficient memory to load file safely. Symbol table: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB. Try loading fewer rows using the maxRows parameter (recommended: ${recommendedMaxRows.toLocaleString()} rows or less).`,
|
|
676
|
+
{
|
|
677
|
+
file: filePath,
|
|
678
|
+
symbolTableSize,
|
|
679
|
+
symbolTableSizeMB: sizeMB,
|
|
680
|
+
estimatedMemoryMB: estimatedMB,
|
|
681
|
+
availableMemoryMB: availableMB,
|
|
682
|
+
totalRows,
|
|
683
|
+
maxRows,
|
|
684
|
+
recommendedMaxRows
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows) {
|
|
690
|
+
const LARGE_SYMBOL_TABLE_WARNING = 500 * 1024 * 1024;
|
|
691
|
+
if (symbolTableSize > LARGE_SYMBOL_TABLE_WARNING && (maxRows === null || maxRows >= totalRows)) {
|
|
692
|
+
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
693
|
+
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
694
|
+
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
695
|
+
console.warn(
|
|
696
|
+
`\u26A0\uFE0F Large symbol table detected (${sizeMB}MB). Loading all ${totalRows.toLocaleString()} rows will use ~${estimatedMB}MB RAM. Consider using the maxRows parameter for better performance and lower memory usage.`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
var init_memoryUtils = __esm({
|
|
701
|
+
"src/util/memoryUtils.js"() {
|
|
702
|
+
init_QvdErrors();
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// src/util/validationUtils.js
|
|
707
|
+
function validateSymbolTableSizeEarly(symbolTableLength, filePath) {
|
|
708
|
+
const MAX_SYMBOL_TABLE_SIZE = 500 * 1024 * 1024;
|
|
709
|
+
if (symbolTableLength > MAX_SYMBOL_TABLE_SIZE) {
|
|
710
|
+
const sizeMB = Math.round(symbolTableLength / 1024 / 1024);
|
|
711
|
+
const maxMB = Math.round(MAX_SYMBOL_TABLE_SIZE / 1024 / 1024);
|
|
712
|
+
throw new QvdValidationError(
|
|
713
|
+
`Symbol table too large (${sizeMB}MB exceeds ${maxMB}MB limit). This QVD file contains extremely high-cardinality fields. Even with symbol filtering, this file cannot be loaded efficiently. Consider: (1) using a subset of the data, (2) aggregating high-cardinality fields, or (3) using a different data format.`,
|
|
714
|
+
{
|
|
715
|
+
file: filePath,
|
|
716
|
+
symbolTableSize: symbolTableLength,
|
|
717
|
+
symbolTableSizeMB: sizeMB,
|
|
718
|
+
maxAllowed: MAX_SYMBOL_TABLE_SIZE,
|
|
719
|
+
maxAllowedMB: maxMB
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function validateSymbolTableSize(symbolTableLength, filePath, totalRows) {
|
|
725
|
+
const ABSOLUTE_MAX_SYMBOL_TABLE = 2 * 1024 * 1024 * 1024;
|
|
726
|
+
if (symbolTableLength > ABSOLUTE_MAX_SYMBOL_TABLE) {
|
|
727
|
+
const sizeMB = Math.round(symbolTableLength / 1024 / 1024);
|
|
728
|
+
const maxMB = Math.round(ABSOLUTE_MAX_SYMBOL_TABLE / 1024 / 1024);
|
|
729
|
+
throw new QvdValidationError(
|
|
730
|
+
`Symbol table exceeds absolute maximum size (${sizeMB}MB > ${maxMB}MB). This QVD file has pathological cardinality (likely a data modeling issue). Even with Phase 2.5 optimization, files this large cannot be processed safely. Consider: (1) aggregating high-cardinality fields, (2) splitting the data, or (3) using a different format.`,
|
|
731
|
+
{
|
|
732
|
+
file: filePath,
|
|
733
|
+
symbolTableSize: symbolTableLength,
|
|
734
|
+
symbolTableSizeMB: sizeMB,
|
|
735
|
+
maxAllowed: ABSOLUTE_MAX_SYMBOL_TABLE,
|
|
736
|
+
maxAllowedMB: maxMB,
|
|
737
|
+
totalRows
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function validateFieldMetadata(field, symbolBufferLength, filePath) {
|
|
743
|
+
const symbolsOffset = parseInt(field["Offset"], 10);
|
|
744
|
+
const symbolsLength = parseInt(field["Length"], 10);
|
|
745
|
+
if (isNaN(symbolsOffset) || !Number.isSafeInteger(symbolsOffset) || symbolsOffset < 0) {
|
|
746
|
+
throw new QvdCorruptedError("Invalid symbol offset", {
|
|
747
|
+
field: field["FieldName"],
|
|
748
|
+
offset: symbolsOffset,
|
|
749
|
+
file: filePath,
|
|
750
|
+
stage: "parseSymbolTable"
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
if (isNaN(symbolsLength) || !Number.isSafeInteger(symbolsLength) || symbolsLength < 0) {
|
|
754
|
+
throw new QvdCorruptedError("Invalid symbol length", {
|
|
755
|
+
field: field["FieldName"],
|
|
756
|
+
length: symbolsLength,
|
|
757
|
+
file: filePath,
|
|
758
|
+
stage: "parseSymbolTable"
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (symbolsOffset + symbolsLength > symbolBufferLength) {
|
|
762
|
+
throw new QvdCorruptedError("Symbol data extends beyond buffer", {
|
|
763
|
+
field: field["FieldName"],
|
|
764
|
+
offset: symbolsOffset,
|
|
765
|
+
length: symbolsLength,
|
|
766
|
+
bufferSize: symbolBufferLength,
|
|
767
|
+
file: filePath,
|
|
768
|
+
stage: "parseSymbolTable"
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath) {
|
|
773
|
+
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
774
|
+
throw new QvdCorruptedError("Invalid record byte size", {
|
|
775
|
+
recordSize,
|
|
776
|
+
file: filePath,
|
|
777
|
+
stage: "parseIndexTable"
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
if (isNaN(totalRows) || !Number.isSafeInteger(totalRows) || totalRows < 0) {
|
|
781
|
+
throw new QvdCorruptedError("Invalid number of records", {
|
|
782
|
+
totalRows,
|
|
783
|
+
file: filePath,
|
|
784
|
+
stage: "parseIndexTable"
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
if (recordSize === 0 && totalRows > 0) {
|
|
788
|
+
throw new QvdCorruptedError("Record byte size cannot be zero when records exist", {
|
|
789
|
+
recordSize,
|
|
790
|
+
totalRows,
|
|
791
|
+
file: filePath,
|
|
792
|
+
stage: "parseIndexTable"
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
if (recordSize > 1048576) {
|
|
796
|
+
throw new QvdCorruptedError("Record byte size exceeds maximum", {
|
|
797
|
+
recordSize,
|
|
798
|
+
maxSize: 1048576,
|
|
799
|
+
file: filePath,
|
|
800
|
+
stage: "parseIndexTable"
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (isNaN(indexTableLength) || !Number.isSafeInteger(indexTableLength) || indexTableLength < 0) {
|
|
804
|
+
throw new QvdCorruptedError("Invalid index table length", {
|
|
805
|
+
length: indexTableLength,
|
|
806
|
+
file: filePath,
|
|
807
|
+
stage: "parseIndexTable"
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
if (indexTableOffset > bufferLength) {
|
|
811
|
+
throw new QvdCorruptedError("Index table offset beyond buffer", {
|
|
812
|
+
indexTableOffset,
|
|
813
|
+
bufferSize: bufferLength,
|
|
814
|
+
file: filePath,
|
|
815
|
+
stage: "parseIndexTable"
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
const maxReasonableOverage = 100 * 1024 * 1024;
|
|
819
|
+
if (indexTableLength > bufferLength + maxReasonableOverage) {
|
|
820
|
+
throw new QvdCorruptedError("Index table length unreasonably large", {
|
|
821
|
+
indexTableLength,
|
|
822
|
+
bufferSize: bufferLength,
|
|
823
|
+
file: filePath,
|
|
824
|
+
stage: "parseIndexTable"
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
const requiredIndexBytes = rowsToLoad * recordSize;
|
|
828
|
+
if (indexTableLength < requiredIndexBytes) {
|
|
829
|
+
throw new QvdCorruptedError("Index table length smaller than required", {
|
|
830
|
+
indexTableLength,
|
|
831
|
+
requiredBytes: requiredIndexBytes,
|
|
832
|
+
rowsToLoad,
|
|
833
|
+
recordSize,
|
|
834
|
+
file: filePath,
|
|
835
|
+
stage: "parseIndexTable"
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function validateFieldBitMetadata(field, recordSize, filePath) {
|
|
840
|
+
const bitOffset = parseInt(field["BitOffset"], 10);
|
|
841
|
+
const bitWidth = parseInt(field["BitWidth"], 10);
|
|
842
|
+
if (isNaN(bitOffset) || !Number.isSafeInteger(bitOffset) || bitOffset < 0) {
|
|
843
|
+
throw new QvdCorruptedError("Invalid bit offset", {
|
|
844
|
+
field: field["FieldName"],
|
|
845
|
+
bitOffset,
|
|
846
|
+
file: filePath,
|
|
847
|
+
stage: "parseIndexTable"
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
if (isNaN(bitWidth) || !Number.isSafeInteger(bitWidth) || bitWidth < 0) {
|
|
851
|
+
throw new QvdCorruptedError("Invalid bit width", {
|
|
852
|
+
field: field["FieldName"],
|
|
853
|
+
bitWidth,
|
|
854
|
+
file: filePath,
|
|
855
|
+
stage: "parseIndexTable"
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
const recordSizeInBits = recordSize * 8;
|
|
859
|
+
if (bitOffset + bitWidth > recordSizeInBits) {
|
|
860
|
+
throw new QvdCorruptedError("Bit field extends beyond record size", {
|
|
861
|
+
field: field["FieldName"],
|
|
862
|
+
bitOffset,
|
|
863
|
+
bitWidth,
|
|
864
|
+
recordSizeInBits,
|
|
865
|
+
file: filePath,
|
|
866
|
+
stage: "parseIndexTable"
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
var init_validationUtils = __esm({
|
|
871
|
+
"src/util/validationUtils.js"() {
|
|
872
|
+
init_QvdErrors();
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// src/util/symbolParser.js
|
|
877
|
+
function parseIntegerSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
878
|
+
if (pointer + 4 > bufferLength) {
|
|
879
|
+
throw new QvdCorruptedError("Buffer overflow reading integer symbol", {
|
|
880
|
+
field: fieldName,
|
|
881
|
+
pointer,
|
|
882
|
+
bufferSize: bufferLength,
|
|
883
|
+
file: filePath,
|
|
884
|
+
stage: "parseSymbolTable"
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
const byteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 4));
|
|
888
|
+
const value = Buffer.from(byteData).readIntLE(0, byteData.length);
|
|
889
|
+
return { symbol: QvdSymbol.fromIntValue(value), bytesRead: 4 };
|
|
890
|
+
}
|
|
891
|
+
function parseDoubleSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
892
|
+
if (pointer + 8 > bufferLength) {
|
|
893
|
+
throw new QvdCorruptedError("Buffer overflow reading double symbol", {
|
|
894
|
+
field: fieldName,
|
|
895
|
+
pointer,
|
|
896
|
+
bufferSize: bufferLength,
|
|
897
|
+
file: filePath,
|
|
898
|
+
stage: "parseSymbolTable"
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
const byteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 8));
|
|
902
|
+
const value = Buffer.from(byteData).readDoubleLE(0);
|
|
903
|
+
return { symbol: QvdSymbol.fromDoubleValue(value), bytesRead: 8 };
|
|
904
|
+
}
|
|
905
|
+
function parseStringSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
906
|
+
const startPointer = pointer;
|
|
907
|
+
const maxStringLength = 1048576;
|
|
908
|
+
let stringLength = 0;
|
|
909
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
910
|
+
if (stringLength >= maxStringLength) {
|
|
911
|
+
throw new QvdCorruptedError("String symbol exceeds maximum length", {
|
|
912
|
+
field: fieldName,
|
|
913
|
+
maxLength: maxStringLength,
|
|
914
|
+
file: filePath,
|
|
915
|
+
stage: "parseSymbolTable"
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
pointer++;
|
|
919
|
+
stringLength++;
|
|
920
|
+
}
|
|
921
|
+
if (pointer >= bufferLength) {
|
|
922
|
+
throw new QvdCorruptedError("String symbol not null-terminated", {
|
|
923
|
+
field: fieldName,
|
|
924
|
+
pointer,
|
|
925
|
+
bufferSize: bufferLength,
|
|
926
|
+
file: filePath,
|
|
927
|
+
stage: "parseSymbolTable"
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
const value = symbolBuffer.subarray(startPointer, pointer).toString("utf-8");
|
|
931
|
+
return { symbol: QvdSymbol.fromStringValue(value), bytesRead: pointer - startPointer + 1 };
|
|
932
|
+
}
|
|
933
|
+
function skipStringSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
934
|
+
const startPointer = pointer;
|
|
935
|
+
const maxStringLength = 1048576;
|
|
936
|
+
let stringLength = 0;
|
|
937
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
938
|
+
if (stringLength >= maxStringLength) {
|
|
939
|
+
throw new QvdCorruptedError("String symbol exceeds maximum length", {
|
|
940
|
+
field: fieldName,
|
|
941
|
+
maxLength: maxStringLength,
|
|
942
|
+
file: filePath,
|
|
943
|
+
stage: "parseSymbolTable"
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
pointer++;
|
|
947
|
+
stringLength++;
|
|
948
|
+
}
|
|
949
|
+
if (pointer >= bufferLength) {
|
|
950
|
+
throw new QvdCorruptedError("String symbol not null-terminated", {
|
|
951
|
+
field: fieldName,
|
|
952
|
+
pointer,
|
|
953
|
+
bufferSize: bufferLength,
|
|
954
|
+
file: filePath,
|
|
955
|
+
stage: "parseSymbolTable"
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return pointer - startPointer + 1;
|
|
959
|
+
}
|
|
960
|
+
function parseDualIntegerSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
961
|
+
if (pointer + 4 > bufferLength) {
|
|
962
|
+
throw new QvdCorruptedError("Buffer overflow reading dual integer symbol", {
|
|
963
|
+
field: fieldName,
|
|
964
|
+
pointer,
|
|
965
|
+
bufferSize: bufferLength,
|
|
966
|
+
file: filePath,
|
|
967
|
+
stage: "parseSymbolTable"
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
const intByteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 4));
|
|
971
|
+
const intValue = Buffer.from(intByteData).readIntLE(0, intByteData.length);
|
|
972
|
+
pointer += 4;
|
|
973
|
+
const stringStart = pointer;
|
|
974
|
+
const maxStringLength = 1048576;
|
|
975
|
+
let stringLength = 0;
|
|
976
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
977
|
+
if (stringLength >= maxStringLength) {
|
|
978
|
+
throw new QvdCorruptedError("Dual string symbol exceeds maximum length", {
|
|
979
|
+
field: fieldName,
|
|
980
|
+
maxLength: maxStringLength,
|
|
981
|
+
file: filePath,
|
|
982
|
+
stage: "parseSymbolTable"
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
pointer++;
|
|
986
|
+
stringLength++;
|
|
987
|
+
}
|
|
988
|
+
if (pointer >= bufferLength) {
|
|
989
|
+
throw new QvdCorruptedError("Dual string symbol not null-terminated", {
|
|
990
|
+
field: fieldName,
|
|
991
|
+
pointer,
|
|
992
|
+
bufferSize: bufferLength,
|
|
993
|
+
file: filePath,
|
|
994
|
+
stage: "parseSymbolTable"
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
const stringValue = symbolBuffer.subarray(stringStart, pointer).toString("utf-8");
|
|
998
|
+
return { symbol: QvdSymbol.fromDualIntValue(intValue, stringValue), bytesRead: pointer - (stringStart - 4) + 1 };
|
|
999
|
+
}
|
|
1000
|
+
function parseDualDoubleSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath) {
|
|
1001
|
+
if (pointer + 8 > bufferLength) {
|
|
1002
|
+
throw new QvdCorruptedError("Buffer overflow reading dual double symbol", {
|
|
1003
|
+
field: fieldName,
|
|
1004
|
+
pointer,
|
|
1005
|
+
bufferSize: bufferLength,
|
|
1006
|
+
file: filePath,
|
|
1007
|
+
stage: "parseSymbolTable"
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
const doubleByteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 8));
|
|
1011
|
+
const doubleValue = Buffer.from(doubleByteData).readDoubleLE(0);
|
|
1012
|
+
pointer += 8;
|
|
1013
|
+
const stringStart = pointer;
|
|
1014
|
+
const maxStringLength = 1048576;
|
|
1015
|
+
let stringLength = 0;
|
|
1016
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
1017
|
+
if (stringLength >= maxStringLength) {
|
|
1018
|
+
throw new QvdCorruptedError("Dual string symbol exceeds maximum length", {
|
|
1019
|
+
field: fieldName,
|
|
1020
|
+
maxLength: maxStringLength,
|
|
1021
|
+
file: filePath,
|
|
1022
|
+
stage: "parseSymbolTable"
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
pointer++;
|
|
1026
|
+
stringLength++;
|
|
1027
|
+
}
|
|
1028
|
+
if (pointer >= bufferLength) {
|
|
1029
|
+
throw new QvdCorruptedError("Dual string symbol not null-terminated", {
|
|
1030
|
+
field: fieldName,
|
|
1031
|
+
pointer,
|
|
1032
|
+
bufferSize: bufferLength,
|
|
1033
|
+
file: filePath,
|
|
1034
|
+
stage: "parseSymbolTable"
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
const stringValue = symbolBuffer.subarray(stringStart, pointer).toString("utf-8");
|
|
1038
|
+
return {
|
|
1039
|
+
symbol: QvdSymbol.fromDualDoubleValue(doubleValue, stringValue),
|
|
1040
|
+
bytesRead: pointer - (stringStart - 8) + 1
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
function skipDualSymbol(symbolBuffer, pointer, bufferLength, numericBytes, fieldName, filePath) {
|
|
1044
|
+
if (pointer + numericBytes > bufferLength) {
|
|
1045
|
+
throw new QvdCorruptedError(`Buffer overflow reading dual symbol`, {
|
|
1046
|
+
field: fieldName,
|
|
1047
|
+
pointer,
|
|
1048
|
+
bufferSize: bufferLength,
|
|
1049
|
+
file: filePath,
|
|
1050
|
+
stage: "parseSymbolTable"
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
const startPointer = pointer;
|
|
1054
|
+
pointer += numericBytes;
|
|
1055
|
+
const maxStringLength = 1048576;
|
|
1056
|
+
let stringLength = 0;
|
|
1057
|
+
while (pointer < bufferLength && symbolBuffer[pointer] !== 0) {
|
|
1058
|
+
if (stringLength >= maxStringLength) {
|
|
1059
|
+
throw new QvdCorruptedError("Dual string symbol exceeds maximum length", {
|
|
1060
|
+
field: fieldName,
|
|
1061
|
+
maxLength: maxStringLength,
|
|
1062
|
+
file: filePath,
|
|
1063
|
+
stage: "parseSymbolTable"
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
pointer++;
|
|
1067
|
+
stringLength++;
|
|
1068
|
+
}
|
|
1069
|
+
if (pointer >= bufferLength) {
|
|
1070
|
+
throw new QvdCorruptedError("Dual string symbol not null-terminated", {
|
|
1071
|
+
field: fieldName,
|
|
1072
|
+
pointer,
|
|
1073
|
+
bufferSize: bufferLength,
|
|
1074
|
+
file: filePath,
|
|
1075
|
+
stage: "parseSymbolTable"
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
return pointer - startPointer + 1;
|
|
1079
|
+
}
|
|
1080
|
+
function parseSymbol(typeByte, symbolBuffer, pointer, bufferLength, fieldName, filePath, shouldParse) {
|
|
1081
|
+
switch (typeByte) {
|
|
1082
|
+
case 1: {
|
|
1083
|
+
if (shouldParse) {
|
|
1084
|
+
return parseIntegerSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1085
|
+
}
|
|
1086
|
+
return { symbol: null, bytesRead: 4 };
|
|
1087
|
+
}
|
|
1088
|
+
case 2: {
|
|
1089
|
+
if (shouldParse) {
|
|
1090
|
+
return parseDoubleSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1091
|
+
}
|
|
1092
|
+
return { symbol: null, bytesRead: 8 };
|
|
1093
|
+
}
|
|
1094
|
+
case 4: {
|
|
1095
|
+
if (shouldParse) {
|
|
1096
|
+
return parseStringSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1097
|
+
}
|
|
1098
|
+
const bytesRead = skipStringSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1099
|
+
return { symbol: null, bytesRead };
|
|
1100
|
+
}
|
|
1101
|
+
case 5: {
|
|
1102
|
+
if (shouldParse) {
|
|
1103
|
+
return parseDualIntegerSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1104
|
+
}
|
|
1105
|
+
const bytesRead = skipDualSymbol(symbolBuffer, pointer, bufferLength, 4, fieldName, filePath);
|
|
1106
|
+
return { symbol: null, bytesRead };
|
|
1107
|
+
}
|
|
1108
|
+
case 6: {
|
|
1109
|
+
if (shouldParse) {
|
|
1110
|
+
return parseDualDoubleSymbol(symbolBuffer, pointer, bufferLength, fieldName, filePath);
|
|
1111
|
+
}
|
|
1112
|
+
const bytesRead = skipDualSymbol(symbolBuffer, pointer, bufferLength, 8, fieldName, filePath);
|
|
1113
|
+
return { symbol: null, bytesRead };
|
|
1114
|
+
}
|
|
1115
|
+
default: {
|
|
1116
|
+
throw new QvdParseError("Unknown symbol type byte", {
|
|
1117
|
+
typeByte: typeByte.toString(16),
|
|
1118
|
+
offset: pointer - 1,
|
|
1119
|
+
file: filePath,
|
|
1120
|
+
stage: "parseSymbolTable"
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
var init_symbolParser = __esm({
|
|
1126
|
+
"src/util/symbolParser.js"() {
|
|
1127
|
+
init_QvdSymbol();
|
|
1128
|
+
init_QvdErrors();
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
638
1132
|
// src/QvdFileReader.js
|
|
639
1133
|
var QvdFileReader_exports = {};
|
|
640
1134
|
__export(QvdFileReader_exports, {
|
|
@@ -643,10 +1137,13 @@ __export(QvdFileReader_exports, {
|
|
|
643
1137
|
var QvdFileReader;
|
|
644
1138
|
var init_QvdFileReader = __esm({
|
|
645
1139
|
"src/QvdFileReader.js"() {
|
|
646
|
-
init_QvdSymbol();
|
|
647
1140
|
init_QvdDataFrame();
|
|
648
1141
|
init_QvdErrors();
|
|
649
1142
|
init_validatePath();
|
|
1143
|
+
init_bitUtils();
|
|
1144
|
+
init_memoryUtils();
|
|
1145
|
+
init_validationUtils();
|
|
1146
|
+
init_symbolParser();
|
|
650
1147
|
QvdFileReader = class {
|
|
651
1148
|
/**
|
|
652
1149
|
* Constructs a new QVD file parser.
|
|
@@ -744,6 +1241,7 @@ var init_QvdFileReader = __esm({
|
|
|
744
1241
|
const recordSize = parseInt(headerObj["QvdTableHeader"]["RecordByteSize"], 10);
|
|
745
1242
|
const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
|
|
746
1243
|
const rowsToLoad = Math.min(maxRows, totalRows);
|
|
1244
|
+
validateSymbolTableSizeEarly(symbolTableLength, this._path);
|
|
747
1245
|
const indexTableBytesToRead = rowsToLoad * recordSize;
|
|
748
1246
|
const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
|
|
749
1247
|
const fd = await fs2.promises.open(this._path, "r");
|
|
@@ -793,11 +1291,80 @@ var init_QvdFileReader = __esm({
|
|
|
793
1291
|
this._symbolTableOffset = headerEndIndex;
|
|
794
1292
|
this._indexTableOffset = this._symbolTableOffset + parseInt(this._header["QvdTableHeader"]["Offset"], 10);
|
|
795
1293
|
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Estimates the memory usage for loading a QVD file with given parameters.
|
|
1296
|
+
* Takes into account Phase 2.5 optimization which skips parsing unused symbols.
|
|
1297
|
+
*
|
|
1298
|
+
* @param {number} symbolTableSize Size of symbol table in bytes
|
|
1299
|
+
* @param {number|null} maxRows Maximum number of rows to load (null = all rows)
|
|
1300
|
+
* @param {number} totalRows Total number of rows in the file
|
|
1301
|
+
* @returns {number} Estimated memory usage in bytes
|
|
1302
|
+
* @private
|
|
1303
|
+
*/
|
|
1304
|
+
_estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
|
|
1305
|
+
return estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Analyzes the index table to determine which symbols are actually needed.
|
|
1309
|
+
* This is used for two-pass symbol filtering optimization.
|
|
1310
|
+
*
|
|
1311
|
+
* @param {number} maxRows The maximum number of rows to analyze.
|
|
1312
|
+
* @returns {Promise<Map<string, Set<number>>>} Map of field names to Set of needed symbol indices.
|
|
1313
|
+
* @private
|
|
1314
|
+
*/
|
|
1315
|
+
async _analyzeIndexTableSymbolUsage(maxRows) {
|
|
1316
|
+
if (!this._buffer || !this._header || !this._indexTableOffset) {
|
|
1317
|
+
throw new QvdCorruptedError(
|
|
1318
|
+
"The QVD file has not been loaded in the proper order or has not been loaded at all.",
|
|
1319
|
+
{
|
|
1320
|
+
file: this._path,
|
|
1321
|
+
stage: "analyzeIndexTableSymbolUsage"
|
|
1322
|
+
}
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
let fields = this._header["QvdTableHeader"]["Fields"]["QvdFieldHeader"];
|
|
1326
|
+
if (!Array.isArray(fields)) {
|
|
1327
|
+
fields = [fields];
|
|
1328
|
+
}
|
|
1329
|
+
const recordSize = parseInt(this._header["QvdTableHeader"]["RecordByteSize"], 10);
|
|
1330
|
+
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1331
|
+
const rowsToAnalyze = Math.min(maxRows, totalRows);
|
|
1332
|
+
const indexTableLength = parseInt(this._header["QvdTableHeader"]["Length"], 10);
|
|
1333
|
+
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1334
|
+
const symbolUsage = /* @__PURE__ */ new Map();
|
|
1335
|
+
fields.forEach((field) => {
|
|
1336
|
+
symbolUsage.set(field["FieldName"], /* @__PURE__ */ new Set());
|
|
1337
|
+
});
|
|
1338
|
+
for (let pointer = 0, rowCount = 0; pointer < indexBuffer.length && rowCount < rowsToAnalyze; pointer += recordSize, rowCount++) {
|
|
1339
|
+
const bytes = new Int32Array(indexBuffer.subarray(pointer, pointer + recordSize));
|
|
1340
|
+
bytes.reverse();
|
|
1341
|
+
const mask = bytes.reduce((bits, byte) => bits + ("00000000" + byte.toString(2)).slice(-8), "").split("").reverse().map((bit) => parseInt(bit));
|
|
1342
|
+
fields.forEach((field) => {
|
|
1343
|
+
const bitOffset = parseInt(field["BitOffset"], 10);
|
|
1344
|
+
const bitWidth = parseInt(field["BitWidth"], 10);
|
|
1345
|
+
const bias = parseInt(field["Bias"], 10);
|
|
1346
|
+
let symbolIndex;
|
|
1347
|
+
if (bitWidth === 0) {
|
|
1348
|
+
symbolIndex = 0;
|
|
1349
|
+
} else {
|
|
1350
|
+
symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
|
|
1351
|
+
}
|
|
1352
|
+
symbolIndex -= bias;
|
|
1353
|
+
const fieldName = field["FieldName"];
|
|
1354
|
+
symbolUsage.get(fieldName)?.add(symbolIndex);
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
return symbolUsage;
|
|
1358
|
+
}
|
|
796
1359
|
/**
|
|
797
1360
|
* Parses the symbol table of the QVD file. This method is part of the parsing process
|
|
798
1361
|
* and should not be called directly.
|
|
1362
|
+
*
|
|
1363
|
+
* @param {Map<string, Set<number>>|null} symbolsToKeep Optional map of field names to symbol indices to keep.
|
|
1364
|
+
* If provided, only these symbols will be parsed (two-pass filtering optimization).
|
|
1365
|
+
* @param {number|null} maxRows Optional maximum number of rows being loaded (for memory estimation).
|
|
799
1366
|
*/
|
|
800
|
-
async _parseSymbolTable() {
|
|
1367
|
+
async _parseSymbolTable(symbolsToKeep = null, maxRows = null) {
|
|
801
1368
|
if (!this._buffer || !this._header || !this._symbolTableOffset || !this._indexTableOffset) {
|
|
802
1369
|
throw new QvdCorruptedError(
|
|
803
1370
|
"The QVD file has not been loaded in the proper order or has not been loaded at all.",
|
|
@@ -809,192 +1376,40 @@ var init_QvdFileReader = __esm({
|
|
|
809
1376
|
}
|
|
810
1377
|
let fields = this._header["QvdTableHeader"]["Fields"]["QvdFieldHeader"];
|
|
811
1378
|
const symbolBuffer = this._buffer.subarray(this._symbolTableOffset, this._indexTableOffset);
|
|
1379
|
+
const symbolTableSize = symbolBuffer.length;
|
|
1380
|
+
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1381
|
+
validateSymbolTableSize(symbolTableSize, this._path, totalRows);
|
|
1382
|
+
validateMemoryAvailability(symbolTableSize, maxRows, totalRows, this._path);
|
|
1383
|
+
warnLargeSymbolTable(symbolTableSize, maxRows, totalRows);
|
|
812
1384
|
if (!Array.isArray(fields)) {
|
|
813
1385
|
fields = [fields];
|
|
814
1386
|
}
|
|
815
1387
|
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
|
-
}
|
|
1388
|
+
validateFieldMetadata(field, symbolBuffer.length, this._path);
|
|
844
1389
|
}
|
|
845
1390
|
this._symbolTable = fields.map((field) => {
|
|
846
1391
|
const symbolsOffset = parseInt(field["Offset"], 10);
|
|
847
1392
|
const symbolsLength = parseInt(field["Length"], 10);
|
|
1393
|
+
const fieldName = field["FieldName"];
|
|
1394
|
+
const neededSymbols = symbolsToKeep ? symbolsToKeep.get(fieldName) : null;
|
|
1395
|
+
const filteringEnabled = neededSymbols !== null;
|
|
848
1396
|
const symbols = [];
|
|
1397
|
+
let symbolIndex = 0;
|
|
849
1398
|
for (let pointer = symbolsOffset; pointer < symbolsOffset + symbolsLength; pointer++) {
|
|
850
1399
|
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
|
-
}
|
|
1400
|
+
const shouldKeepSymbol = !filteringEnabled || !!(neededSymbols && neededSymbols.has(symbolIndex));
|
|
1401
|
+
const { symbol, bytesRead } = parseSymbol(
|
|
1402
|
+
typeByte,
|
|
1403
|
+
symbolBuffer,
|
|
1404
|
+
pointer,
|
|
1405
|
+
symbolBuffer.length,
|
|
1406
|
+
fieldName,
|
|
1407
|
+
this._path,
|
|
1408
|
+
shouldKeepSymbol
|
|
1409
|
+
);
|
|
1410
|
+
symbols.push(symbol);
|
|
1411
|
+
pointer += bytesRead - 1;
|
|
1412
|
+
symbolIndex++;
|
|
998
1413
|
}
|
|
999
1414
|
return symbols;
|
|
1000
1415
|
});
|
|
@@ -1006,10 +1421,7 @@ var init_QvdFileReader = __esm({
|
|
|
1006
1421
|
* @return {Number} The integer value
|
|
1007
1422
|
*/
|
|
1008
1423
|
_convertBitsToInt32(bits) {
|
|
1009
|
-
|
|
1010
|
-
return 0;
|
|
1011
|
-
}
|
|
1012
|
-
return bits.reduce((value, bit, index) => value += bit * Math.pow(2, index), 0);
|
|
1424
|
+
return convertBitsToInt32(bits);
|
|
1013
1425
|
}
|
|
1014
1426
|
/**
|
|
1015
1427
|
* Parses the bit stuffed index table of the QVD file. This method is part of the parsing process
|
|
@@ -1032,105 +1444,21 @@ var init_QvdFileReader = __esm({
|
|
|
1032
1444
|
fields = [fields];
|
|
1033
1445
|
}
|
|
1034
1446
|
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
1447
|
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
1448
|
const rowsToLoad = maxRows !== null ? Math.min(maxRows, totalRows) : totalRows;
|
|
1067
1449
|
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
|
-
}
|
|
1450
|
+
validateIndexTableMetadata(
|
|
1451
|
+
recordSize,
|
|
1452
|
+
totalRows,
|
|
1453
|
+
indexTableLength,
|
|
1454
|
+
this._indexTableOffset,
|
|
1455
|
+
this._buffer.length,
|
|
1456
|
+
rowsToLoad,
|
|
1457
|
+
this._path
|
|
1458
|
+
);
|
|
1103
1459
|
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1104
1460
|
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
|
-
}
|
|
1461
|
+
validateFieldBitMetadata(field, recordSize, this._path);
|
|
1134
1462
|
}
|
|
1135
1463
|
this._indexTable = [];
|
|
1136
1464
|
for (let pointer = 0, rowCount = 0; pointer < indexBuffer.length && rowCount < rowsToLoad; pointer += recordSize, rowCount++) {
|
|
@@ -1172,7 +1500,19 @@ var init_QvdFileReader = __esm({
|
|
|
1172
1500
|
async load(maxRows = null) {
|
|
1173
1501
|
await this._readData(maxRows);
|
|
1174
1502
|
await this._parseHeader();
|
|
1175
|
-
|
|
1503
|
+
let symbolsToKeep = null;
|
|
1504
|
+
if (maxRows !== null && this._header) {
|
|
1505
|
+
const symbolTableLength = parseInt(this._header["QvdTableHeader"]["Offset"], 10);
|
|
1506
|
+
const SYMBOL_FILTERING_THRESHOLD = 50 * 1024 * 1024;
|
|
1507
|
+
if (symbolTableLength > SYMBOL_FILTERING_THRESHOLD) {
|
|
1508
|
+
symbolsToKeep = await this._analyzeIndexTableSymbolUsage(maxRows);
|
|
1509
|
+
const totalSymbols = Array.from(symbolsToKeep.values()).reduce((sum, set) => sum + set.size, 0);
|
|
1510
|
+
console.log(
|
|
1511
|
+
`[Phase 2.5 Optimization] Using stream-and-skip parsing: keeping ${totalSymbols} symbols from ${(symbolTableLength / 1024 / 1024).toFixed(1)}MB symbol table`
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
await this._parseSymbolTable(symbolsToKeep, maxRows);
|
|
1176
1516
|
await this._parseIndexTable(maxRows);
|
|
1177
1517
|
assert(this._header, "The QVD file header has not been parsed.");
|
|
1178
1518
|
assert(this._symbolTable, "The QVD file symbol table has not been parsed.");
|