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/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
- const symbolsOffset = parseInt(field["Offset"], 10);
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
- switch (typeByte) {
862
- case 1: {
863
- if (pointer + 4 > symbolBuffer.length) {
864
- throw new exports.QvdCorruptedError("Buffer overflow reading integer symbol", {
865
- field: field["FieldName"],
866
- pointer,
867
- bufferSize: symbolBuffer.length,
868
- file: this._path,
869
- stage: "parseSymbolTable"
870
- });
871
- }
872
- const byteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 4));
873
- const value = Buffer.from(byteData).readIntLE(0, byteData.length);
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
- if (bits.length === 0) {
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
- if (isNaN(indexTableLength) || !Number.isSafeInteger(indexTableLength) || indexTableLength < 0) {
1079
- throw new exports.QvdCorruptedError("Invalid index table length", {
1080
- length: indexTableLength,
1081
- file: this._path,
1082
- stage: "parseIndexTable"
1083
- });
1084
- }
1085
- if (this._indexTableOffset > this._buffer.length) {
1086
- throw new exports.QvdCorruptedError("Index table offset beyond buffer", {
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
- const bitOffset = parseInt(field["BitOffset"], 10);
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
- await this._parseSymbolTable();
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 = { allowedDir: options.allowedDir };
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
  /**