qvdjs 2.0.0 → 2.0.1

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.js CHANGED
@@ -1086,8 +1086,12 @@ function fieldGeometry(bitOffset, bitWidth) {
1086
1086
  byteCount: bitWidth === 0 ? 0 : shift + bitWidth + 7 >>> 3
1087
1087
  };
1088
1088
  }
1089
- function decodeIndexColumn(buffer, recordSize, rowCount, bitOffset, bitWidth, bias, out) {
1089
+ function decodeIndexColumn(buffer, recordSize, rowCount, bitOffset, bitWidth, bias, out, bounds = null) {
1090
+ const symbolCount = bounds === null ? Infinity : bounds.symbolCount;
1090
1091
  if (bitWidth === 0) {
1092
+ if (bounds !== null && rowCount > 0 && bias >= symbolCount) {
1093
+ refuse2(bounds, 0, bias, bias);
1094
+ }
1091
1095
  out.fill(bias, 0, rowCount);
1092
1096
  return out;
1093
1097
  }
@@ -1101,10 +1105,25 @@ function decodeIndexColumn(buffer, recordSize, rowCount, bitOffset, bitWidth, bi
1101
1105
  if (byteCount > 2) acc += buffer[base + 2] * 65536;
1102
1106
  if (byteCount > 3) acc += buffer[base + 3] * 16777216;
1103
1107
  if (byteCount > 4) acc += buffer[base + 4] * 4294967296;
1104
- out[row] = Math.floor(acc / divisor) % modulus + bias;
1108
+ const index = Math.floor(acc / divisor) % modulus + bias;
1109
+ if ((index >= symbolCount || index < 0 && index !== bias) && bounds !== null) {
1110
+ refuse2(bounds, row, index, bias);
1111
+ }
1112
+ out[row] = index;
1105
1113
  }
1106
1114
  return out;
1107
1115
  }
1116
+ function refuse2(bounds, row, index, bias) {
1117
+ throw new QvdCorruptedError("Symbol index out of range", {
1118
+ field: bounds.field,
1119
+ row: bounds.firstRow + row,
1120
+ symbolIndex: index,
1121
+ symbolCount: bounds.symbolCount,
1122
+ bias,
1123
+ file: bounds.file,
1124
+ stage: "parseIndexTable"
1125
+ });
1126
+ }
1108
1127
  function writeBitField(buffer, recordBase, geometry, value) {
1109
1128
  const { byteStart, shift, byteCount } = geometry;
1110
1129
  if (byteCount === 0) {
@@ -1121,10 +1140,12 @@ function writeBitField(buffer, recordBase, geometry, value) {
1121
1140
  var MAX_BIT_WIDTH, POW2;
1122
1141
  var init_bitUtils = __esm({
1123
1142
  "src/util/bitUtils.js"() {
1143
+ init_QvdErrors();
1124
1144
  MAX_BIT_WIDTH = 31;
1125
1145
  POW2 = Array.from({ length: 41 }, (_, exponent) => 2 ** exponent);
1126
1146
  __name(fieldGeometry, "fieldGeometry");
1127
1147
  __name(decodeIndexColumn, "decodeIndexColumn");
1148
+ __name(refuse2, "refuse");
1128
1149
  __name(writeBitField, "writeBitField");
1129
1150
  }
1130
1151
  });
@@ -2558,6 +2579,15 @@ function validateFieldBitMetadata(field, recordSize, filePath) {
2558
2579
  stage: "parseIndexTable"
2559
2580
  });
2560
2581
  }
2582
+ if (bias < -2147483648 || bias + 2 ** bitWidth - 1 > 2 ** 31 - 1) {
2583
+ throw new QvdCorruptedError("Bias out of range", {
2584
+ field: field["FieldName"],
2585
+ bias,
2586
+ bitWidth,
2587
+ file: filePath,
2588
+ stage: "parseIndexTable"
2589
+ });
2590
+ }
2561
2591
  const recordSizeInBits = recordSize * 8;
2562
2592
  if (bitOffset + bitWidth > recordSizeInBits) {
2563
2593
  throw new QvdCorruptedError("Bit field extends beyond record size", {
@@ -2685,7 +2715,10 @@ function parseFieldSymbols(symbolBuffer, start, end, keep, fieldName, filePath)
2685
2715
  }
2686
2716
  return { numbers, texts };
2687
2717
  }
2688
- var MAX_TEXT_BYTES;
2718
+ function countFieldSymbols(symbolBuffer, start, end, fieldName, filePath) {
2719
+ return parseFieldSymbols(symbolBuffer, start, end, DECODE_NOTHING, fieldName, filePath).numbers.length;
2720
+ }
2721
+ var MAX_TEXT_BYTES, DECODE_NOTHING;
2689
2722
  var init_symbolParser = __esm({
2690
2723
  "src/util/symbolParser.js"() {
2691
2724
  init_QvdErrors();
@@ -2693,6 +2726,8 @@ var init_symbolParser = __esm({
2693
2726
  __name(textEnd, "textEnd");
2694
2727
  __name(overflow, "overflow");
2695
2728
  __name(parseFieldSymbols, "parseFieldSymbols");
2729
+ DECODE_NOTHING = /* @__PURE__ */ new Set();
2730
+ __name(countFieldSymbols, "countFieldSymbols");
2696
2731
  }
2697
2732
  });
2698
2733
 
@@ -3206,7 +3241,7 @@ async function* chunksFrom(handle, chunkSize, failed) {
3206
3241
  position += bytesRead;
3207
3242
  }
3208
3243
  }
3209
- var MAX_HEADER_SIZE, READ_CHUNK_SIZE, ANALYSIS_SLICE_ROWS, QvdFileReader;
3244
+ var MAX_HEADER_SIZE, READ_CHUNK_SIZE, ANALYSIS_SLICE_ROWS, COUNT_SYMBOLS_PAST, QvdFileReader;
3210
3245
  var init_QvdFileReader = __esm({
3211
3246
  "src/QvdFileReader.js"() {
3212
3247
  init_QvdDataFrame();
@@ -3224,6 +3259,7 @@ var init_QvdFileReader = __esm({
3224
3259
  MAX_HEADER_SIZE = 16 * 1024 * 1024;
3225
3260
  READ_CHUNK_SIZE = 512 * 1024 * 1024;
3226
3261
  ANALYSIS_SLICE_ROWS = 65536;
3262
+ COUNT_SYMBOLS_PAST = 65536;
3227
3263
  __name(chunksFrom, "chunksFrom");
3228
3264
  QvdFileReader = class {
3229
3265
  static {
@@ -3625,6 +3661,7 @@ var init_QvdFileReader = __esm({
3625
3661
  }
3626
3662
  const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
3627
3663
  const headerBuffer = this._buffer.subarray(headerBeginIndex, headerEndIndex);
3664
+ this._fieldBitMetadataValidated = false;
3628
3665
  this._header = await xml2.parseStringPromise(headerBuffer.toString(), { explicitArray: false });
3629
3666
  if (!this._header) {
3630
3667
  throw new QvdParseError("The XML header could not be parsed.", {
@@ -3670,8 +3707,8 @@ var init_QvdFileReader = __esm({
3670
3707
  * @param {QvdRowWindow} window The rows of interest, as file row indices.
3671
3708
  * @param {string} stage Stage name for any error raised here.
3672
3709
  * @return {{fields: Array<any>, recordSize: number, totalRows: number, rowsToLoad: number,
3673
- * indexBuffer: Buffer}} The record geometry. `indexBuffer` starts at the window's first
3674
- * record, so the decoder always counts from zero.
3710
+ * indexBuffer: Buffer, firstRow: number}} The record geometry. `indexBuffer` starts at the
3711
+ * window's first record, file row `firstRow`, so the decoder always counts from zero.
3675
3712
  * @private
3676
3713
  */
3677
3714
  _planIndexTable(window, stage) {
@@ -3717,7 +3754,7 @@ var init_QvdFileReader = __esm({
3717
3754
  rowsToLoad === 0 || recordSize === 0 || Math.floor(indexBuffer.length / recordSize) >= rowsToLoad,
3718
3755
  `The index table holds ${Math.floor(indexBuffer.length / (recordSize || 1))} whole records but ${rowsToLoad} were validated as present.`
3719
3756
  );
3720
- return { fields, recordSize, totalRows, rowsToLoad, indexBuffer };
3757
+ return { fields, recordSize, totalRows, rowsToLoad, indexBuffer, firstRow };
3721
3758
  }
3722
3759
  /**
3723
3760
  * Analyzes the index table to determine which symbols are actually needed.
@@ -3744,6 +3781,9 @@ var init_QvdFileReader = __esm({
3744
3781
  const bitOffset = parseInt(field["BitOffset"], 10);
3745
3782
  const bitWidth = parseInt(field["BitWidth"], 10);
3746
3783
  const bias = parseInt(field["Bias"], 10);
3784
+ const length = parseInt(field["Length"], 10);
3785
+ let indexLimit = Number.isSafeInteger(length) && length >= 0 ? Math.ceil(length / 2) : Infinity;
3786
+ let counted = false;
3747
3787
  for (let first = 0; first < rowsToLoad; first += sliceRows) {
3748
3788
  const count = Math.min(sliceRows, rowsToLoad - first);
3749
3789
  decodeIndexColumn(
@@ -3756,15 +3796,53 @@ var init_QvdFileReader = __esm({
3756
3796
  column
3757
3797
  );
3758
3798
  for (let row = 0; row < count; row++) {
3759
- if (column[row] >= 0) {
3799
+ if (column[row] >= 0 && column[row] < indexLimit) {
3760
3800
  needed.add(column[row]);
3761
3801
  }
3762
3802
  }
3803
+ if (!counted && needed.size > COUNT_SYMBOLS_PAST) {
3804
+ counted = true;
3805
+ indexLimit = Math.min(indexLimit, this._countFieldSymbols(field));
3806
+ for (const index of needed) {
3807
+ if (index >= indexLimit) {
3808
+ needed.delete(index);
3809
+ }
3810
+ }
3811
+ }
3763
3812
  }
3764
3813
  this._emitProgress("symbol-analysis", position + 1, fields.length);
3765
3814
  });
3766
3815
  return symbolUsage;
3767
3816
  }
3817
+ /**
3818
+ * How many symbols a field holds, for the symbol-usage pass, which runs before the symbols are parsed.
3819
+ *
3820
+ * The count is `countFieldSymbols`, the parse itself told to decode nothing, so it is the count
3821
+ * `_parseSymbolTable` will produce and `_parseIndexTable` will check against. The field's area is
3822
+ * validated first, as `_parseSymbolTable` would, so a damaged `Offset` or `Length` is reported the
3823
+ * same way wherever it is met.
3824
+ *
3825
+ * @param {any} field The field's header.
3826
+ * @return {number} Its symbols.
3827
+ * @throws {QvdCorruptedError} If the area is not inside the symbol table, or a symbol runs past it.
3828
+ * @private
3829
+ */
3830
+ _countFieldSymbols(field) {
3831
+ assert3(
3832
+ this._buffer && this._symbolTableOffset && this._indexTableOffset,
3833
+ "The QVD file has not been read before its symbols were counted."
3834
+ );
3835
+ const symbolBuffer = this._buffer.subarray(this._symbolTableOffset, this._indexTableOffset);
3836
+ validateFieldMetadata(field, symbolBuffer.length, this._path);
3837
+ const offset = parseInt(field["Offset"], 10);
3838
+ return countFieldSymbols(
3839
+ symbolBuffer,
3840
+ offset,
3841
+ offset + parseInt(field["Length"], 10),
3842
+ field["FieldName"],
3843
+ this._path
3844
+ );
3845
+ }
3768
3846
  /**
3769
3847
  * Parses the symbol table of the QVD file. This method is part of the parsing process
3770
3848
  * and should not be called directly.
@@ -3850,12 +3928,19 @@ var init_QvdFileReader = __esm({
3850
3928
  * `base += recordSize`, so decoding rows k to k+n is a question of where the buffer slice starts
3851
3929
  * and how many iterations run. Nothing about the decoder changed to support it.
3852
3930
  *
3931
+ * Every index is checked as it is decoded. One that addresses neither a symbol of its field nor
3932
+ * NULL throws a `QvdCorruptedError` naming the file row (#125). Here and not where rows or columns
3933
+ * are built, because every read decodes through this method and a columnar read hands its codes
3934
+ * straight to the caller. Rows outside the window are not decoded, so they are not checked.
3935
+ *
3853
3936
  * @param {QvdRowWindow} window The rows to decode.
3937
+ * @throws {QvdCorruptedError} If an index in the window addresses neither a symbol nor NULL.
3854
3938
  */
3855
3939
  async _parseIndexTable(window) {
3856
- const { fields, recordSize, rowsToLoad, indexBuffer } = this._planIndexTable(window, "parseIndexTable");
3857
- this._rowsDecoded = rowsToLoad;
3858
- this._indexColumns = fields.map((field, position) => {
3940
+ const { fields, recordSize, rowsToLoad, indexBuffer, firstRow } = this._planIndexTable(window, "parseIndexTable");
3941
+ assert3(this._symbolTable, "The QVD file symbol table has not been parsed.");
3942
+ const symbolTable = this._symbolTable;
3943
+ const columns = fields.map((field, position) => {
3859
3944
  this._throwIfAborted();
3860
3945
  const column = decodeIndexColumn(
3861
3946
  indexBuffer,
@@ -3864,11 +3949,14 @@ var init_QvdFileReader = __esm({
3864
3949
  parseInt(field["BitOffset"], 10),
3865
3950
  parseInt(field["BitWidth"], 10),
3866
3951
  parseInt(field["Bias"], 10),
3867
- new Int32Array(rowsToLoad)
3952
+ new Int32Array(rowsToLoad),
3953
+ { symbolCount: symbolTable[position].numbers.length, field: field["FieldName"], file: this._path, firstRow }
3868
3954
  );
3869
3955
  this._emitProgress("index-table", position + 1, fields.length);
3870
3956
  return column;
3871
3957
  });
3958
+ this._indexColumns = columns;
3959
+ this._rowsDecoded = rowsToLoad;
3872
3960
  }
3873
3961
  /**
3874
3962
  * Reads the file's schema and header metadata, without touching the symbol or index tables.
@@ -3997,7 +4085,8 @@ var init_QvdFileReader = __esm({
3997
4085
  * `chunkSize * 2`, and why the heap it needs is twice what one chunk suggests.
3998
4086
  *
3999
4087
  * A window covering no rows yields nothing at all, rather than one empty frame - so
4000
- * `for await` over an exhausted offset does nothing, which is what a paging loop wants.
4088
+ * `for await` over an exhausted offset does nothing, which is what a paging loop wants. Its header
4089
+ * is still checked, as every read of a file's rows checks it.
4001
4090
  *
4002
4091
  * @param {number|null|{offset?: number, limit?: number|null, maxRows?: number|null}} window
4003
4092
  * The rows to cover, in the same spellings `load()` accepts.
@@ -4015,6 +4104,10 @@ var init_QvdFileReader = __esm({
4015
4104
  const liveRows = { rows: chunkSize * 2, perChunk: 2 };
4016
4105
  const rows = normaliseWindow(window, this._path);
4017
4106
  const prepared = await this._prepare(rows, liveRows);
4107
+ if (prepared.rowsAvailable === 0) {
4108
+ this._planIndexTable({ offset: prepared.offset, limit: 0 }, "parseIndexTable");
4109
+ return;
4110
+ }
4018
4111
  for (let done = 0; done < prepared.rowsAvailable; done += chunkSize) {
4019
4112
  this._throwIfAborted();
4020
4113
  const count = Math.min(chunkSize, prepared.rowsAvailable - done);