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