qvdjs 0.2.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 ADDED
@@ -0,0 +1,1615 @@
1
+ 'use strict';
2
+
3
+ var path2 = require('path');
4
+ var fs2 = require('fs');
5
+ var crypto = require('crypto');
6
+ var xml2 = require('xml2js');
7
+ var assert = require('assert');
8
+
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
+
11
+ var path2__default = /*#__PURE__*/_interopDefault(path2);
12
+ var fs2__default = /*#__PURE__*/_interopDefault(fs2);
13
+ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
14
+ var xml2__default = /*#__PURE__*/_interopDefault(xml2);
15
+ var assert__default = /*#__PURE__*/_interopDefault(assert);
16
+
17
+ var __defProp = Object.defineProperty;
18
+ var __getOwnPropNames = Object.getOwnPropertyNames;
19
+ var __esm = (fn, res) => function __init() {
20
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
21
+ };
22
+ var __export = (target, all) => {
23
+ for (var name in all)
24
+ __defProp(target, name, { get: all[name], enumerable: true });
25
+ };
26
+
27
+ // src/QvdErrors.js
28
+ exports.QvdError = void 0; exports.QvdParseError = void 0; exports.QvdValidationError = void 0; exports.QvdIOError = void 0; exports.QvdCorruptedError = void 0; exports.QvdSecurityError = void 0;
29
+ var init_QvdErrors = __esm({
30
+ "src/QvdErrors.js"() {
31
+ exports.QvdError = class extends Error {
32
+ /**
33
+ * Constructs a new QVD error.
34
+ *
35
+ * @param {string} message The error message.
36
+ * @param {string} code The error code.
37
+ * @param {Object} [context={}] Additional context about the error.
38
+ */
39
+ constructor(message, code, context = {}) {
40
+ super(message);
41
+ this.name = this.constructor.name;
42
+ this.code = code;
43
+ this.context = context;
44
+ Error.captureStackTrace(this, this.constructor);
45
+ }
46
+ };
47
+ exports.QvdParseError = class extends exports.QvdError {
48
+ /**
49
+ * Constructs a new QVD parse error.
50
+ *
51
+ * @param {string} message The error message.
52
+ * @param {Object} [context={}] Additional context about the error.
53
+ */
54
+ constructor(message, context = {}) {
55
+ super(message, "QVD_PARSE_ERROR", context);
56
+ }
57
+ };
58
+ exports.QvdValidationError = class extends exports.QvdError {
59
+ /**
60
+ * Constructs a new QVD validation error.
61
+ *
62
+ * @param {string} message The error message.
63
+ * @param {Object} [context={}] Additional context about the error.
64
+ */
65
+ constructor(message, context = {}) {
66
+ super(message, "QVD_VALIDATION_ERROR", context);
67
+ }
68
+ };
69
+ exports.QvdIOError = class extends exports.QvdError {
70
+ /**
71
+ * Constructs a new QVD IO error.
72
+ *
73
+ * @param {string} message The error message.
74
+ * @param {Object} [context={}] Additional context about the error.
75
+ */
76
+ constructor(message, context = {}) {
77
+ super(message, "QVD_IO_ERROR", context);
78
+ }
79
+ };
80
+ exports.QvdCorruptedError = class extends exports.QvdError {
81
+ /**
82
+ * Constructs a new QVD corrupted error.
83
+ *
84
+ * @param {string} message The error message.
85
+ * @param {Object} [context={}] Additional context about the error.
86
+ */
87
+ constructor(message, context = {}) {
88
+ super(message, "QVD_CORRUPTED_ERROR", context);
89
+ }
90
+ };
91
+ exports.QvdSecurityError = class extends exports.QvdError {
92
+ /**
93
+ * Constructs a new QVD security error.
94
+ *
95
+ * @param {string} message The error message.
96
+ * @param {Object} [context={}] Additional context about the error.
97
+ */
98
+ constructor(message, context = {}) {
99
+ super(message, "QVD_SECURITY_ERROR", context);
100
+ }
101
+ };
102
+ }
103
+ });
104
+
105
+ // src/QvdSymbol.js
106
+ exports.QvdSymbol = void 0;
107
+ var init_QvdSymbol = __esm({
108
+ "src/QvdSymbol.js"() {
109
+ init_QvdErrors();
110
+ exports.QvdSymbol = class _QvdSymbol {
111
+ /**
112
+ * Constructs a new QVD symbol.
113
+ *
114
+ * @param {number|null} intValue The integer value.
115
+ * @param {number|null} doubleValue The double value.
116
+ * @param {string|null} stringValue The string value.
117
+ */
118
+ constructor(intValue, doubleValue, stringValue) {
119
+ this._intValue = intValue;
120
+ this._doubleValue = doubleValue;
121
+ this._stringValue = stringValue;
122
+ }
123
+ /**
124
+ * Returns the integer value of this symbol.
125
+ *
126
+ * @return {number|null} The integer value.
127
+ */
128
+ get intValue() {
129
+ return this._intValue;
130
+ }
131
+ /**
132
+ * Returns the double value of this symbol.
133
+ *
134
+ * @return {number|null} The double value.
135
+ */
136
+ get doubleValue() {
137
+ return this._doubleValue;
138
+ }
139
+ /**
140
+ * Returns the string value of this symbol.
141
+ *
142
+ * @return {string|null} The string value.
143
+ */
144
+ get stringValue() {
145
+ return this._stringValue;
146
+ }
147
+ /**
148
+ * Retrieves the primary value of this symbol. The primary value is descriptive raw value.
149
+ * It is either the string value, the integer value or the double value, prioritized in this order.
150
+ *
151
+ * @return {number|string|null} The primary value.
152
+ */
153
+ toPrimaryValue() {
154
+ if (null != this._stringValue) {
155
+ return this._stringValue;
156
+ } else if (null != this._intValue) {
157
+ return this._intValue;
158
+ } else if (null != this._doubleValue) {
159
+ return this._doubleValue;
160
+ } else {
161
+ return null;
162
+ }
163
+ }
164
+ /**
165
+ * Converts the symbol to its byte representation.
166
+ *
167
+ * @return {Buffer} The byte representation of the symbol.
168
+ */
169
+ toByteRepresentation() {
170
+ if (this._intValue !== null && this._stringValue !== null) {
171
+ const intBuffer = Buffer.alloc(4);
172
+ intBuffer.writeInt32LE(this._intValue);
173
+ const stringBuffer = Buffer.concat([Buffer.from(this._stringValue, "utf-8"), Buffer.from([0])]);
174
+ return Buffer.concat([Buffer.from([5]), intBuffer, stringBuffer]);
175
+ } else if (this._doubleValue !== null && this._stringValue !== null) {
176
+ const floatBuffer = Buffer.alloc(8);
177
+ floatBuffer.writeDoubleLE(this._doubleValue);
178
+ const stringBuffer = Buffer.concat([Buffer.from(this._stringValue, "utf-8"), Buffer.from([0])]);
179
+ return Buffer.concat([Buffer.from([6]), floatBuffer, stringBuffer]);
180
+ } else if (this._intValue !== null) {
181
+ const buffer = Buffer.alloc(4);
182
+ buffer.writeInt32LE(this._intValue);
183
+ return Buffer.concat([Buffer.from([1]), buffer]);
184
+ } else if (this._doubleValue !== null) {
185
+ const buffer = Buffer.alloc(8);
186
+ buffer.writeDoubleLE(this._doubleValue);
187
+ return Buffer.concat([Buffer.from([2]), buffer]);
188
+ } else if (this._stringValue !== null) {
189
+ const buffer = Buffer.concat([Buffer.from(this._stringValue, "utf-8"), Buffer.from([0])]);
190
+ return Buffer.concat([Buffer.from([4]), buffer]);
191
+ } else {
192
+ throw new exports.QvdValidationError("The symbol does not contain any value.", {
193
+ intValue: this._intValue,
194
+ doubleValue: this._doubleValue,
195
+ stringValue: this._stringValue
196
+ });
197
+ }
198
+ }
199
+ /**
200
+ * Checks if this symbol is equal to another symbol.
201
+ *
202
+ * @param {*} value The object to compare with.
203
+ * @return {boolean} True if the objects are equal, false otherwise.
204
+ */
205
+ equals(value) {
206
+ if (!(value instanceof _QvdSymbol)) {
207
+ return false;
208
+ }
209
+ return this._intValue === value.intValue && this._doubleValue === value.doubleValue && this._stringValue === value.stringValue;
210
+ }
211
+ /**
212
+ * Constructs a pure integer value symbol.
213
+ *
214
+ * @param {number} intValue The integer value.
215
+ * @return {QvdSymbol} The constructed value symbol.
216
+ */
217
+ static fromIntValue(intValue) {
218
+ return new _QvdSymbol(intValue, null, null);
219
+ }
220
+ /**
221
+ * Constructs a pure double value symbol.
222
+ *
223
+ * @param {number} doubleValue The double value.
224
+ * @return {QvdSymbol} The constructed value symbol.
225
+ */
226
+ static fromDoubleValue(doubleValue) {
227
+ return new _QvdSymbol(null, doubleValue, null);
228
+ }
229
+ /**
230
+ * Constructs a pure string value symbol.
231
+ *
232
+ * @param {string} stringValue The string value.
233
+ * @return {QvdSymbol} The constructed value symbol.
234
+ */
235
+ static fromStringValue(stringValue) {
236
+ return new _QvdSymbol(null, null, stringValue);
237
+ }
238
+ /**
239
+ * Constructs a dual value symbol from an integer and a string value.
240
+ *
241
+ * @param {number} intValue The integer value.
242
+ * @param {string} stringValue The string value.
243
+ * @return {QvdSymbol} The constructed value symbol.
244
+ */
245
+ static fromDualIntValue(intValue, stringValue) {
246
+ return new _QvdSymbol(intValue, null, stringValue);
247
+ }
248
+ /**
249
+ * Constructs a dual value symbol from a double and a string value.
250
+ *
251
+ * @param {number} doubleValue The double value.
252
+ * @param {string} stringValue The string value.
253
+ * @return {QvdSymbol} The constructed value symbol.
254
+ */
255
+ static fromDualDoubleValue(doubleValue, stringValue) {
256
+ return new _QvdSymbol(null, doubleValue, stringValue);
257
+ }
258
+ };
259
+ }
260
+ });
261
+ function validatePath(filePath, allowedDir) {
262
+ if (filePath.includes("\0")) {
263
+ throw new exports.QvdSecurityError("Path traversal detected: Null byte in path", {
264
+ path: filePath,
265
+ reason: "null_byte"
266
+ });
267
+ }
268
+ const baseDir = allowedDir || process.cwd();
269
+ const resolvedBaseDir = path2__default.default.resolve(baseDir);
270
+ const resolvedPath = path2__default.default.resolve(filePath);
271
+ const isCaseInsensitiveFS = process.platform === "win32" || process.platform === "darwin";
272
+ const baseForComparison = isCaseInsensitiveFS ? resolvedBaseDir.toLowerCase() : resolvedBaseDir;
273
+ const pathForComparison = isCaseInsensitiveFS ? resolvedPath.toLowerCase() : resolvedPath;
274
+ if (!pathForComparison.startsWith(baseForComparison + path2__default.default.sep) && pathForComparison !== baseForComparison) {
275
+ throw new exports.QvdSecurityError("Path traversal detected: Access denied", {
276
+ path: filePath,
277
+ resolvedPath,
278
+ allowedDir: resolvedBaseDir,
279
+ reason: "outside_allowed_directory"
280
+ });
281
+ }
282
+ return resolvedPath;
283
+ }
284
+ var init_validatePath = __esm({
285
+ "src/util/validatePath.js"() {
286
+ init_QvdErrors();
287
+ }
288
+ });
289
+
290
+ // src/QvdFileWriter.js
291
+ var QvdFileWriter_exports = {};
292
+ __export(QvdFileWriter_exports, {
293
+ QvdFileWriter: () => exports.QvdFileWriter
294
+ });
295
+ exports.QvdFileWriter = void 0;
296
+ var init_QvdFileWriter = __esm({
297
+ "src/QvdFileWriter.js"() {
298
+ init_QvdSymbol();
299
+ init_validatePath();
300
+ exports.QvdFileWriter = class _QvdFileWriter {
301
+ /**
302
+ * Constructs a new QVD file writer.
303
+ *
304
+ * @param {string} filePath The path to the QVD file to write.
305
+ * @param {QvdDataFrame} df The data frame to write to the QVD file.
306
+ * @param {Object} [options={}] Options for the writer.
307
+ * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
308
+ * path must be within this directory. Defaults to current working directory.
309
+ * @param {Function} [options.onProgress] Optional progress callback function.
310
+ */
311
+ constructor(filePath, df, options = {}) {
312
+ const { allowedDir, onProgress } = options;
313
+ this._path = validatePath(filePath, allowedDir);
314
+ this._df = df;
315
+ this._onProgress = onProgress;
316
+ this._header = null;
317
+ this._symbolBuffer = null;
318
+ this._symbolTable = null;
319
+ this._symbolTableMetadata = null;
320
+ this._indexBuffer = null;
321
+ this._indexTable = null;
322
+ this._indexTableMetadata = null;
323
+ this._recordByteSize = null;
324
+ }
325
+ /**
326
+ * Emits a progress event if a callback is registered.
327
+ *
328
+ * @param {string} stage The current stage of the operation.
329
+ * @param {number} current The current progress value.
330
+ * @param {number} total The total progress value.
331
+ */
332
+ _emitProgress(stage, current, total) {
333
+ if (this._onProgress) {
334
+ const percent = total > 0 ? Math.round(current / total * 100) : 100;
335
+ this._onProgress({
336
+ stage,
337
+ current,
338
+ total,
339
+ percent
340
+ });
341
+ }
342
+ }
343
+ /**
344
+ * Writes the data to the QVD file.
345
+ */
346
+ async _writeData() {
347
+ assert__default.default(this._header, "The QVD file header has not been parsed.");
348
+ assert__default.default(this._symbolBuffer, "The QVD file symbol table has not been parsed.");
349
+ assert__default.default(this._indexBuffer, "The QVD file index table has not been parsed.");
350
+ this._emitProgress("write", 0, 1);
351
+ const headerBuffer = Buffer.concat([Buffer.from(this._header, "utf-8"), Buffer.from([0])]);
352
+ let fd;
353
+ try {
354
+ fd = await fs2__default.default.promises.open(this._path, "w");
355
+ await fd.write(headerBuffer, 0, headerBuffer.length, 0);
356
+ await fd.write(this._symbolBuffer, 0, this._symbolBuffer.length, headerBuffer.length);
357
+ await fd.write(this._indexBuffer, 0, this._indexBuffer.length, headerBuffer.length + this._symbolBuffer.length);
358
+ this._emitProgress("write", 1, 1);
359
+ } finally {
360
+ if (fd) {
361
+ await fd.close();
362
+ }
363
+ }
364
+ }
365
+ /**
366
+ * Builds the XML header of the QVD file.
367
+ */
368
+ _buildHeader() {
369
+ this._emitProgress("header", 0, 1);
370
+ const creationDate = (/* @__PURE__ */ new Date()).toISOString().replace(/T/, " ").replace(/\..+/, "");
371
+ const existingMetadata = this._df.metadata;
372
+ const baseMetadata = existingMetadata ? {
373
+ QvBuildNo: existingMetadata.QvBuildNo || 50667,
374
+ CreatorDoc: existingMetadata.CreatorDoc || crypto__default.default.randomUUID(),
375
+ CreateUtcTime: existingMetadata.CreateUtcTime || creationDate,
376
+ SourceCreateUtcTime: existingMetadata.SourceCreateUtcTime || "",
377
+ SourceFileUtcTime: existingMetadata.SourceFileUtcTime || "",
378
+ SourceFileSize: existingMetadata.SourceFileSize || -1,
379
+ StaleUtcTime: existingMetadata.StaleUtcTime || "",
380
+ TableName: existingMetadata.TableName || path2__default.default.basename(this._path, path2__default.default.extname(this._path)),
381
+ Compression: existingMetadata.Compression || "",
382
+ Comment: existingMetadata.Comment || "",
383
+ EncryptionInfo: existingMetadata.EncryptionInfo || "",
384
+ TableTags: existingMetadata.TableTags || "",
385
+ ProfilingData: existingMetadata.ProfilingData || "",
386
+ Lineage: existingMetadata.Lineage || {
387
+ LineageInfo: {
388
+ Discriminator: "INLINE;",
389
+ Statement: ""
390
+ }
391
+ }
392
+ } : {
393
+ QvBuildNo: 50667,
394
+ CreatorDoc: crypto__default.default.randomUUID(),
395
+ CreateUtcTime: creationDate,
396
+ SourceCreateUtcTime: "",
397
+ SourceFileUtcTime: "",
398
+ SourceFileSize: -1,
399
+ StaleUtcTime: "",
400
+ TableName: path2__default.default.basename(this._path, path2__default.default.extname(this._path)),
401
+ Compression: "",
402
+ Comment: "",
403
+ EncryptionInfo: "",
404
+ TableTags: "",
405
+ ProfilingData: "",
406
+ Lineage: {
407
+ LineageInfo: {
408
+ Discriminator: "INLINE;",
409
+ Statement: ""
410
+ }
411
+ }
412
+ };
413
+ let existingFields = [];
414
+ if (existingMetadata && existingMetadata.Fields && existingMetadata.Fields.QvdFieldHeader) {
415
+ existingFields = Array.isArray(existingMetadata.Fields.QvdFieldHeader) ? existingMetadata.Fields.QvdFieldHeader : [existingMetadata.Fields.QvdFieldHeader];
416
+ }
417
+ const xmlObject = {
418
+ QvdTableHeader: {
419
+ ...baseMetadata,
420
+ Fields: {
421
+ QvdFieldHeader: this._df.columns.map((column, index) => {
422
+ const existingField = existingFields.find((f) => f.FieldName === column);
423
+ return {
424
+ FieldName: column,
425
+ BitOffset: this._indexTableMetadata?.[index][0],
426
+ BitWidth: this._indexTableMetadata?.[index][1],
427
+ Bias: this._indexTableMetadata?.[index][2],
428
+ NoOfSymbols: this._symbolTable?.[index].length,
429
+ Offset: this._symbolTableMetadata?.[index][0],
430
+ Length: this._symbolTableMetadata?.[index][1],
431
+ Comment: existingField?.Comment || "",
432
+ NumberFormat: existingField?.NumberFormat || {
433
+ Type: "UNKNOWN",
434
+ nDec: "0",
435
+ UseThou: "0",
436
+ Fmt: "",
437
+ Dec: "",
438
+ Thou: ""
439
+ },
440
+ Tags: existingField?.Tags || {}
441
+ };
442
+ })
443
+ },
444
+ NoOfRecords: this._indexTable?.length,
445
+ RecordByteSize: this._recordByteSize,
446
+ Offset: this._symbolTableMetadata && this._symbolTableMetadata.length > 0 ? this._symbolTableMetadata[this._symbolTableMetadata.length - 1][0] + this._symbolTableMetadata[this._symbolTableMetadata.length - 1][1] : 0,
447
+ Length: this._indexBuffer?.length
448
+ }
449
+ };
450
+ const builder = new xml2__default.default.Builder({
451
+ renderOpts: {
452
+ pretty: true,
453
+ newline: "\r\n",
454
+ indent: " "
455
+ }
456
+ });
457
+ this._header = builder.buildObject(xmlObject) + "\r\n";
458
+ this._emitProgress("header", 1, 1);
459
+ }
460
+ /**
461
+ * Builds the symbol table of the QVD file.
462
+ * Optimized to build all columns in a single pass through the data.
463
+ */
464
+ _buildSymbolTable() {
465
+ this._symbolTable = [];
466
+ this._symbolTableMetadata = [];
467
+ this._symbolBuffer = Buffer.alloc(0);
468
+ const numColumns = this._df.columns.length;
469
+ this._emitProgress("symbol-table", 0, numColumns);
470
+ const uniqueValuesSets = this._df.columns.map(() => /* @__PURE__ */ new Set());
471
+ this._df.data.forEach((row) => {
472
+ this._df.columns.forEach((column, columnIndex) => {
473
+ const value = row[columnIndex];
474
+ uniqueValuesSets[columnIndex].add(value);
475
+ });
476
+ });
477
+ this._df.columns.forEach((column, columnIndex) => {
478
+ const uniqueValuesSet = uniqueValuesSets[columnIndex];
479
+ const uniqueValues = Array.from(uniqueValuesSet);
480
+ const containsNull = uniqueValues.includes(null) || uniqueValues.includes(void 0);
481
+ const symbols = uniqueValues.filter((value) => value !== null && value !== void 0).map((value) => _QvdFileWriter._convertRawToSymbol(value));
482
+ const currentSymbolBuffer = Buffer.concat(symbols.map((symbol) => symbol.toByteRepresentation()));
483
+ this._symbolBuffer = this._symbolBuffer ? Buffer.concat([this._symbolBuffer, currentSymbolBuffer]) : currentSymbolBuffer;
484
+ const symbolsLength = currentSymbolBuffer.length;
485
+ const symbolsOffset = this._symbolBuffer.length - symbolsLength;
486
+ this._symbolTableMetadata?.push([symbolsOffset, symbolsLength, containsNull]);
487
+ this._symbolTable?.push(symbols);
488
+ this._emitProgress("symbol-table", columnIndex + 1, numColumns);
489
+ });
490
+ }
491
+ /**
492
+ * Builds the index table of the QVD file.
493
+ * Optimized with Map-based lookups for O(1) symbol index retrieval.
494
+ */
495
+ _buildIndexTable() {
496
+ this._indexTable = [];
497
+ this._indexTableMetadata = [];
498
+ this._indexBuffer = Buffer.alloc(0);
499
+ const numRows = this._df.data.length;
500
+ this._emitProgress("index-table", 0, numRows);
501
+ const symbolIndexMaps = this._symbolTable?.map((symbols) => {
502
+ const map = /* @__PURE__ */ new Map();
503
+ symbols.forEach((symbol, idx) => {
504
+ const key = `${symbol.intValue}|${symbol.doubleValue}|${symbol.stringValue}`;
505
+ map.set(key, idx);
506
+ });
507
+ return map;
508
+ });
509
+ let processedRows = 0;
510
+ const progressInterval = Math.max(1, Math.floor(numRows / 100));
511
+ this._df.data.forEach((row) => {
512
+ const indices = this._df.columns.map((column, columnIndex) => {
513
+ const value = row[columnIndex];
514
+ const symbol = _QvdFileWriter._convertRawToSymbol(value);
515
+ const fieldContainsNull = this._symbolTableMetadata?.[columnIndex][2];
516
+ if (symbol === null) {
517
+ return 0;
518
+ } else {
519
+ const key = `${symbol.intValue}|${symbol.doubleValue}|${symbol.stringValue}`;
520
+ const symbolIndex = symbolIndexMaps?.[columnIndex].get(key);
521
+ return fieldContainsNull ? (symbolIndex ?? 0) + 2 : symbolIndex ?? 0;
522
+ }
523
+ });
524
+ const stringIndices = indices.map((index) => {
525
+ const bits = _QvdFileWriter._convertInt32ToBits(index, 32);
526
+ let bitString = bits.join("");
527
+ bitString = bitString.replace(/^0+/, "") || "0";
528
+ return bitString;
529
+ });
530
+ this._indexTable?.push(stringIndices);
531
+ processedRows++;
532
+ if (processedRows % progressInterval === 0 || processedRows === numRows) {
533
+ this._emitProgress("index-table", processedRows, numRows);
534
+ }
535
+ });
536
+ this._df.columns.forEach((column) => {
537
+ const bitOffset = this._indexTableMetadata?.slice(0, this._df.columns.indexOf(column)).reduce((sum, metadata) => sum + metadata[1], 0);
538
+ assert__default.default(this._indexTable, "The QVD file header has not been parsed.");
539
+ const bitWidth = this._indexTable.length > 0 ? Math.max(
540
+ ...this._indexTable.map(
541
+ (indices) => indices[this._df.columns.indexOf(column)].length
542
+ )
543
+ ) : 0;
544
+ const fieldContainsNull = this._symbolTableMetadata?.[this._df.columns.indexOf(column)][2];
545
+ const bias = fieldContainsNull ? -2 : 0;
546
+ this._indexTableMetadata?.push([bitOffset, bitWidth, bias]);
547
+ this._indexTable.forEach((indices) => {
548
+ const bitString = indices[this._df.columns.indexOf(column)];
549
+ const paddedBitString = bitString.padStart(bitWidth, "0");
550
+ indices[this._df.columns.indexOf(column)] = paddedBitString;
551
+ });
552
+ });
553
+ this._indexBuffer = Buffer.concat(
554
+ // @ts-ignore - Buffer array type compatibility
555
+ this._indexTable.map((indices) => {
556
+ indices.reverse();
557
+ const bits = indices.join("");
558
+ const paddingWidth = (8 - bits.length % 8) % 8;
559
+ const paddedBits = bits.padStart(bits.length + paddingWidth, "0");
560
+ const bytes = paddedBits.match(/.{1,8}/g)?.map((byte) => parseInt(byte, 2));
561
+ bytes?.reverse();
562
+ assert__default.default(bytes, "Byte conversion of bit indices failed.");
563
+ return Buffer.from(Uint8Array.from(bytes));
564
+ })
565
+ );
566
+ this._recordByteSize = this._indexTable.length > 0 ? this._indexBuffer.length / this._indexTable.length : 1;
567
+ }
568
+ /**
569
+ * Converts a raw value/literal to a QVD symbol.
570
+ *
571
+ * @param {any} raw The raw value/literal to convert.
572
+ * @return {QvdSymbol|null} The converted QVD symbol.
573
+ */
574
+ static _convertRawToSymbol(raw) {
575
+ if (raw === null || raw === void 0) {
576
+ return null;
577
+ }
578
+ const INT32_MIN = -2147483648;
579
+ const INT32_MAX = 2147483647;
580
+ const isInteger = typeof raw === "number" && Number.isInteger(raw);
581
+ const isFloat = typeof raw === "number" && !Number.isInteger(raw);
582
+ const isWithinInt32Range = typeof raw === "number" && raw >= INT32_MIN && raw <= INT32_MAX;
583
+ if (isInteger && isWithinInt32Range) {
584
+ return exports.QvdSymbol.fromDualIntValue(raw, raw.toString());
585
+ } else if (isFloat || isInteger && !isWithinInt32Range) {
586
+ return exports.QvdSymbol.fromDualDoubleValue(raw, raw.toString());
587
+ } else {
588
+ return exports.QvdSymbol.fromStringValue(raw);
589
+ }
590
+ }
591
+ /**
592
+ * Converts an integer to a list of bits.
593
+ *
594
+ * @param {number} value The integer value to convert.
595
+ * @param {number} width The width of the bit list.
596
+ * @return {Array<number>} The list of bits.
597
+ */
598
+ static _convertInt32ToBits(value, width) {
599
+ return value.toString(2).split("").map((bit) => parseInt(bit)).reverse().concat(new Array(width).fill(0)).slice(0, width).reverse();
600
+ }
601
+ /**
602
+ * Persists the data frame to a QVD file.
603
+ */
604
+ async save() {
605
+ this._buildSymbolTable();
606
+ this._buildIndexTable();
607
+ this._buildHeader();
608
+ await this._writeData();
609
+ }
610
+ };
611
+ }
612
+ });
613
+
614
+ // src/QvdFileReader.js
615
+ var QvdFileReader_exports = {};
616
+ __export(QvdFileReader_exports, {
617
+ QvdFileReader: () => exports.QvdFileReader
618
+ });
619
+ exports.QvdFileReader = void 0;
620
+ var init_QvdFileReader = __esm({
621
+ "src/QvdFileReader.js"() {
622
+ init_QvdSymbol();
623
+ init_QvdDataFrame();
624
+ init_QvdErrors();
625
+ init_validatePath();
626
+ exports.QvdFileReader = class {
627
+ /**
628
+ * Constructs a new QVD file parser.
629
+ *
630
+ * @param {string} filePath The path to the QVD file to load.
631
+ * @param {Object} [options={}] Options for the reader.
632
+ * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
633
+ * path must be within this directory. Defaults to current working directory.
634
+ */
635
+ constructor(filePath, options = {}) {
636
+ const { allowedDir } = options;
637
+ this._path = validatePath(filePath, allowedDir);
638
+ this._buffer = null;
639
+ this._headerOffset = null;
640
+ this._symbolTableOffset = null;
641
+ this._indexTableOffset = null;
642
+ this._header = null;
643
+ this._symbolTable = null;
644
+ this._indexTable = null;
645
+ }
646
+ /**
647
+ * Reads the binary data of the QVD file. This method is part of the parsing process
648
+ * and should not be called directly.
649
+ *
650
+ * @param {number|null} maxRows The maximum number of rows to load. If null, all data is loaded.
651
+ */
652
+ async _readData(maxRows = null) {
653
+ if (maxRows === null) {
654
+ this._buffer = await fs2__default.default.promises.readFile(this._path);
655
+ return;
656
+ }
657
+ const HEADER_DELIMITER = "\r\n\0";
658
+ const CHUNK_SIZE = 64 * 1024;
659
+ const stream = fs2__default.default.createReadStream(this._path, {
660
+ highWaterMark: CHUNK_SIZE
661
+ });
662
+ let headerBuffer = Buffer.alloc(0);
663
+ let headerDelimiterIndex = -1;
664
+ try {
665
+ for await (const chunk of stream) {
666
+ headerBuffer = Buffer.concat([headerBuffer, chunk]);
667
+ headerDelimiterIndex = headerBuffer.indexOf(HEADER_DELIMITER);
668
+ if (headerDelimiterIndex !== -1) {
669
+ stream.destroy();
670
+ break;
671
+ }
672
+ }
673
+ } catch (error) {
674
+ if (error && typeof error === "object" && "code" in error && error.code !== "ERR_STREAM_PREMATURE_CLOSE") {
675
+ throw error;
676
+ }
677
+ }
678
+ if (headerDelimiterIndex === -1) {
679
+ throw new exports.QvdCorruptedError(
680
+ "The XML header section does not exist or is not properly delimited from the binary data.",
681
+ {
682
+ file: this._path,
683
+ stage: "readData"
684
+ }
685
+ );
686
+ }
687
+ const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
688
+ const headerXml = headerBuffer.subarray(0, headerEndIndex).toString();
689
+ const headerObj = await xml2__default.default.parseStringPromise(headerXml, { explicitArray: false });
690
+ if (!headerObj) {
691
+ throw new exports.QvdParseError("The XML header could not be parsed.", {
692
+ file: this._path,
693
+ stage: "readData"
694
+ });
695
+ }
696
+ const symbolTableOffset = headerEndIndex;
697
+ const symbolTableLength = parseInt(headerObj["QvdTableHeader"]["Offset"], 10);
698
+ const indexTableOffset = symbolTableOffset + symbolTableLength;
699
+ const recordSize = parseInt(headerObj["QvdTableHeader"]["RecordByteSize"], 10);
700
+ const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
701
+ const rowsToLoad = Math.min(maxRows, totalRows);
702
+ const indexTableBytesToRead = rowsToLoad * recordSize;
703
+ const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
704
+ const fd = await fs2__default.default.promises.open(this._path, "r");
705
+ try {
706
+ this._buffer = Buffer.allocUnsafe(totalBytesToRead);
707
+ await fd.read(this._buffer, 0, totalBytesToRead, 0);
708
+ } finally {
709
+ await fd.close();
710
+ }
711
+ }
712
+ /**
713
+ * Parses the XML header of the QVD file. This method is part of the parsing process
714
+ * and should not be called directly.
715
+ */
716
+ async _parseHeader() {
717
+ if (!this._buffer) {
718
+ throw new exports.QvdCorruptedError(
719
+ "The QVD file has not been loaded in the proper order or has not been loaded at all.",
720
+ {
721
+ file: this._path,
722
+ stage: "parseHeader"
723
+ }
724
+ );
725
+ }
726
+ const HEADER_DELIMITER = "\r\n\0";
727
+ const headerBeginIndex = 0;
728
+ const headerDelimiterIndex = this._buffer.indexOf(HEADER_DELIMITER, headerBeginIndex);
729
+ if (headerDelimiterIndex === -1) {
730
+ throw new exports.QvdCorruptedError(
731
+ "The XML header section does not exist or is not properly delimited from the binary data.",
732
+ {
733
+ file: this._path,
734
+ stage: "parseHeader"
735
+ }
736
+ );
737
+ }
738
+ const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
739
+ const headerBuffer = this._buffer.subarray(headerBeginIndex, headerEndIndex);
740
+ this._header = await xml2__default.default.parseStringPromise(headerBuffer.toString(), { explicitArray: false });
741
+ if (!this._header) {
742
+ throw new exports.QvdParseError("The XML header could not be parsed.", {
743
+ file: this._path,
744
+ stage: "parseHeader"
745
+ });
746
+ }
747
+ this._headerOffset = headerBeginIndex;
748
+ this._symbolTableOffset = headerEndIndex;
749
+ this._indexTableOffset = this._symbolTableOffset + parseInt(this._header["QvdTableHeader"]["Offset"], 10);
750
+ }
751
+ /**
752
+ * Parses the symbol table of the QVD file. This method is part of the parsing process
753
+ * and should not be called directly.
754
+ */
755
+ async _parseSymbolTable() {
756
+ if (!this._buffer || !this._header || !this._symbolTableOffset || !this._indexTableOffset) {
757
+ throw new exports.QvdCorruptedError(
758
+ "The QVD file has not been loaded in the proper order or has not been loaded at all.",
759
+ {
760
+ file: this._path,
761
+ stage: "parseSymbolTable"
762
+ }
763
+ );
764
+ }
765
+ let fields = this._header["QvdTableHeader"]["Fields"]["QvdFieldHeader"];
766
+ const symbolBuffer = this._buffer.subarray(this._symbolTableOffset, this._indexTableOffset);
767
+ if (!Array.isArray(fields)) {
768
+ fields = [fields];
769
+ }
770
+ for (const field of fields) {
771
+ const symbolsOffset = parseInt(field["Offset"], 10);
772
+ const symbolsLength = parseInt(field["Length"], 10);
773
+ if (isNaN(symbolsOffset) || !Number.isSafeInteger(symbolsOffset) || symbolsOffset < 0) {
774
+ throw new exports.QvdCorruptedError("Invalid symbol offset", {
775
+ field: field["FieldName"],
776
+ offset: symbolsOffset,
777
+ file: this._path,
778
+ stage: "parseSymbolTable"
779
+ });
780
+ }
781
+ if (isNaN(symbolsLength) || !Number.isSafeInteger(symbolsLength) || symbolsLength < 0) {
782
+ throw new exports.QvdCorruptedError("Invalid symbol length", {
783
+ field: field["FieldName"],
784
+ length: symbolsLength,
785
+ file: this._path,
786
+ stage: "parseSymbolTable"
787
+ });
788
+ }
789
+ if (symbolsOffset + symbolsLength > symbolBuffer.length) {
790
+ throw new exports.QvdCorruptedError("Symbol data extends beyond buffer", {
791
+ field: field["FieldName"],
792
+ offset: symbolsOffset,
793
+ length: symbolsLength,
794
+ bufferSize: symbolBuffer.length,
795
+ file: this._path,
796
+ stage: "parseSymbolTable"
797
+ });
798
+ }
799
+ }
800
+ this._symbolTable = fields.map((field) => {
801
+ const symbolsOffset = parseInt(field["Offset"], 10);
802
+ const symbolsLength = parseInt(field["Length"], 10);
803
+ const symbols = [];
804
+ for (let pointer = symbolsOffset; pointer < symbolsOffset + symbolsLength; pointer++) {
805
+ const typeByte = symbolBuffer[pointer++];
806
+ switch (typeByte) {
807
+ case 1: {
808
+ if (pointer + 4 > symbolBuffer.length) {
809
+ throw new exports.QvdCorruptedError("Buffer overflow reading integer symbol", {
810
+ field: field["FieldName"],
811
+ pointer,
812
+ bufferSize: symbolBuffer.length,
813
+ file: this._path,
814
+ stage: "parseSymbolTable"
815
+ });
816
+ }
817
+ const byteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 4));
818
+ const value = Buffer.from(byteData).readIntLE(0, byteData.length);
819
+ pointer += 3;
820
+ symbols.push(exports.QvdSymbol.fromIntValue(value));
821
+ break;
822
+ }
823
+ case 2: {
824
+ if (pointer + 8 > symbolBuffer.length) {
825
+ throw new exports.QvdCorruptedError("Buffer overflow reading double symbol", {
826
+ field: field["FieldName"],
827
+ pointer,
828
+ bufferSize: symbolBuffer.length,
829
+ file: this._path,
830
+ stage: "parseSymbolTable"
831
+ });
832
+ }
833
+ const byteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 8));
834
+ const value = Buffer.from(byteData).readDoubleLE(0);
835
+ pointer += 7;
836
+ symbols.push(exports.QvdSymbol.fromDoubleValue(value));
837
+ break;
838
+ }
839
+ case 4: {
840
+ const byteData = [];
841
+ const maxStringLength = 1048576;
842
+ while (pointer < symbolBuffer.length && symbolBuffer[pointer] !== 0) {
843
+ if (byteData.length >= maxStringLength) {
844
+ throw new exports.QvdCorruptedError("String symbol exceeds maximum length", {
845
+ field: field["FieldName"],
846
+ maxLength: maxStringLength,
847
+ file: this._path,
848
+ stage: "parseSymbolTable"
849
+ });
850
+ }
851
+ byteData.push(symbolBuffer[pointer++]);
852
+ }
853
+ if (pointer >= symbolBuffer.length) {
854
+ throw new exports.QvdCorruptedError("String symbol not null-terminated", {
855
+ field: field["FieldName"],
856
+ pointer,
857
+ bufferSize: symbolBuffer.length,
858
+ file: this._path,
859
+ stage: "parseSymbolTable"
860
+ });
861
+ }
862
+ const value = Buffer.from(byteData).toString("utf-8");
863
+ symbols.push(exports.QvdSymbol.fromStringValue(value));
864
+ break;
865
+ }
866
+ case 5: {
867
+ if (pointer + 4 > symbolBuffer.length) {
868
+ throw new exports.QvdCorruptedError("Buffer overflow reading dual integer symbol", {
869
+ field: field["FieldName"],
870
+ pointer,
871
+ bufferSize: symbolBuffer.length,
872
+ file: this._path,
873
+ stage: "parseSymbolTable"
874
+ });
875
+ }
876
+ const intByteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 4));
877
+ const intValue = Buffer.from(intByteData).readIntLE(0, intByteData.length);
878
+ pointer += 4;
879
+ const stringByteData = [];
880
+ const maxStringLength = 1048576;
881
+ while (pointer < symbolBuffer.length && symbolBuffer[pointer] !== 0) {
882
+ if (stringByteData.length >= maxStringLength) {
883
+ throw new exports.QvdCorruptedError("Dual string symbol exceeds maximum length", {
884
+ field: field["FieldName"],
885
+ maxLength: maxStringLength,
886
+ file: this._path,
887
+ stage: "parseSymbolTable"
888
+ });
889
+ }
890
+ stringByteData.push(symbolBuffer[pointer++]);
891
+ }
892
+ if (pointer >= symbolBuffer.length) {
893
+ throw new exports.QvdCorruptedError("Dual string symbol not null-terminated", {
894
+ field: field["FieldName"],
895
+ pointer,
896
+ bufferSize: symbolBuffer.length,
897
+ file: this._path,
898
+ stage: "parseSymbolTable"
899
+ });
900
+ }
901
+ const stringValue = Buffer.from(stringByteData).toString("utf-8");
902
+ symbols.push(exports.QvdSymbol.fromDualIntValue(intValue, stringValue));
903
+ break;
904
+ }
905
+ case 6: {
906
+ if (pointer + 8 > symbolBuffer.length) {
907
+ throw new exports.QvdCorruptedError("Buffer overflow reading dual double symbol", {
908
+ field: field["FieldName"],
909
+ pointer,
910
+ bufferSize: symbolBuffer.length,
911
+ file: this._path,
912
+ stage: "parseSymbolTable"
913
+ });
914
+ }
915
+ const doubleByteData = new Int32Array(symbolBuffer.subarray(pointer, pointer + 8));
916
+ const doubleValue = Buffer.from(doubleByteData).readDoubleLE(0);
917
+ pointer += 8;
918
+ const stringByteData = [];
919
+ const maxStringLength = 1048576;
920
+ while (pointer < symbolBuffer.length && symbolBuffer[pointer] !== 0) {
921
+ if (stringByteData.length >= maxStringLength) {
922
+ throw new exports.QvdCorruptedError("Dual string symbol exceeds maximum length", {
923
+ field: field["FieldName"],
924
+ maxLength: maxStringLength,
925
+ file: this._path,
926
+ stage: "parseSymbolTable"
927
+ });
928
+ }
929
+ stringByteData.push(symbolBuffer[pointer++]);
930
+ }
931
+ if (pointer >= symbolBuffer.length) {
932
+ throw new exports.QvdCorruptedError("Dual string symbol not null-terminated", {
933
+ field: field["FieldName"],
934
+ pointer,
935
+ bufferSize: symbolBuffer.length,
936
+ file: this._path,
937
+ stage: "parseSymbolTable"
938
+ });
939
+ }
940
+ const stringValue = Buffer.from(stringByteData).toString("utf-8");
941
+ symbols.push(exports.QvdSymbol.fromDualDoubleValue(doubleValue, stringValue));
942
+ break;
943
+ }
944
+ default: {
945
+ throw new exports.QvdParseError("Unknown symbol type byte", {
946
+ typeByte: typeByte.toString(16),
947
+ offset: pointer,
948
+ file: this._path,
949
+ stage: "parseSymbolTable"
950
+ });
951
+ }
952
+ }
953
+ }
954
+ return symbols;
955
+ });
956
+ }
957
+ /**
958
+ * Utility method to convert a bit array to an integer value.
959
+ *
960
+ * @param {Array<number>} bits The bit array
961
+ * @return {Number} The integer value
962
+ */
963
+ _convertBitsToInt32(bits) {
964
+ if (bits.length === 0) {
965
+ return 0;
966
+ }
967
+ return bits.reduce((value, bit, index) => value += bit * Math.pow(2, index), 0);
968
+ }
969
+ /**
970
+ * Parses the bit stuffed index table of the QVD file. This method is part of the parsing process
971
+ * and should not be called directly.
972
+ *
973
+ * @param {number|null} maxRows The maximum number of rows to parse. If null, all rows are parsed.
974
+ */
975
+ async _parseIndexTable(maxRows = null) {
976
+ if (!this._buffer || !this._header || !this._indexTableOffset) {
977
+ throw new exports.QvdCorruptedError(
978
+ "The QVD file has not been loaded in the proper order or has not been loaded at all.",
979
+ {
980
+ file: this._path,
981
+ stage: "parseIndexTable"
982
+ }
983
+ );
984
+ }
985
+ let fields = this._header["QvdTableHeader"]["Fields"]["QvdFieldHeader"];
986
+ if (!Array.isArray(fields)) {
987
+ fields = [fields];
988
+ }
989
+ const recordSize = parseInt(this._header["QvdTableHeader"]["RecordByteSize"], 10);
990
+ if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
991
+ throw new exports.QvdCorruptedError("Invalid record byte size", {
992
+ recordSize,
993
+ file: this._path,
994
+ stage: "parseIndexTable"
995
+ });
996
+ }
997
+ const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
998
+ if (isNaN(totalRows) || !Number.isSafeInteger(totalRows) || totalRows < 0) {
999
+ throw new exports.QvdCorruptedError("Invalid number of records", {
1000
+ totalRows,
1001
+ file: this._path,
1002
+ stage: "parseIndexTable"
1003
+ });
1004
+ }
1005
+ if (recordSize === 0 && totalRows > 0) {
1006
+ throw new exports.QvdCorruptedError("Record byte size cannot be zero when records exist", {
1007
+ recordSize,
1008
+ totalRows,
1009
+ file: this._path,
1010
+ stage: "parseIndexTable"
1011
+ });
1012
+ }
1013
+ if (recordSize > 1048576) {
1014
+ throw new exports.QvdCorruptedError("Record byte size exceeds maximum", {
1015
+ recordSize,
1016
+ maxSize: 1048576,
1017
+ file: this._path,
1018
+ stage: "parseIndexTable"
1019
+ });
1020
+ }
1021
+ const rowsToLoad = maxRows !== null ? Math.min(maxRows, totalRows) : totalRows;
1022
+ const indexTableLength = parseInt(this._header["QvdTableHeader"]["Length"], 10);
1023
+ if (isNaN(indexTableLength) || !Number.isSafeInteger(indexTableLength) || indexTableLength < 0) {
1024
+ throw new exports.QvdCorruptedError("Invalid index table length", {
1025
+ length: indexTableLength,
1026
+ file: this._path,
1027
+ stage: "parseIndexTable"
1028
+ });
1029
+ }
1030
+ if (this._indexTableOffset > this._buffer.length) {
1031
+ throw new exports.QvdCorruptedError("Index table offset beyond buffer", {
1032
+ indexTableOffset: this._indexTableOffset,
1033
+ bufferSize: this._buffer.length,
1034
+ file: this._path,
1035
+ stage: "parseIndexTable"
1036
+ });
1037
+ }
1038
+ const maxReasonableOverage = 100 * 1024 * 1024;
1039
+ if (indexTableLength > this._buffer.length + maxReasonableOverage) {
1040
+ throw new exports.QvdCorruptedError("Index table length unreasonably large", {
1041
+ indexTableLength,
1042
+ bufferSize: this._buffer.length,
1043
+ file: this._path,
1044
+ stage: "parseIndexTable"
1045
+ });
1046
+ }
1047
+ const requiredIndexBytes = rowsToLoad * recordSize;
1048
+ if (indexTableLength < requiredIndexBytes) {
1049
+ throw new exports.QvdCorruptedError("Index table length smaller than required", {
1050
+ indexTableLength,
1051
+ requiredBytes: requiredIndexBytes,
1052
+ rowsToLoad,
1053
+ recordSize,
1054
+ file: this._path,
1055
+ stage: "parseIndexTable"
1056
+ });
1057
+ }
1058
+ const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
1059
+ for (const field of fields) {
1060
+ const bitOffset = parseInt(field["BitOffset"], 10);
1061
+ const bitWidth = parseInt(field["BitWidth"], 10);
1062
+ if (isNaN(bitOffset) || !Number.isSafeInteger(bitOffset) || bitOffset < 0) {
1063
+ throw new exports.QvdCorruptedError("Invalid bit offset", {
1064
+ field: field["FieldName"],
1065
+ bitOffset,
1066
+ file: this._path,
1067
+ stage: "parseIndexTable"
1068
+ });
1069
+ }
1070
+ if (isNaN(bitWidth) || !Number.isSafeInteger(bitWidth) || bitWidth < 0) {
1071
+ throw new exports.QvdCorruptedError("Invalid bit width", {
1072
+ field: field["FieldName"],
1073
+ bitWidth,
1074
+ file: this._path,
1075
+ stage: "parseIndexTable"
1076
+ });
1077
+ }
1078
+ const recordSizeInBits = recordSize * 8;
1079
+ if (bitOffset + bitWidth > recordSizeInBits) {
1080
+ throw new exports.QvdCorruptedError("Bit field extends beyond record size", {
1081
+ field: field["FieldName"],
1082
+ bitOffset,
1083
+ bitWidth,
1084
+ recordSizeInBits,
1085
+ file: this._path,
1086
+ stage: "parseIndexTable"
1087
+ });
1088
+ }
1089
+ }
1090
+ this._indexTable = [];
1091
+ for (let pointer = 0, rowCount = 0; pointer < indexBuffer.length && rowCount < rowsToLoad; pointer += recordSize, rowCount++) {
1092
+ if (pointer + recordSize > indexBuffer.length) {
1093
+ throw new exports.QvdCorruptedError("Buffer overflow reading index table record", {
1094
+ pointer,
1095
+ recordSize,
1096
+ bufferSize: indexBuffer.length,
1097
+ file: this._path,
1098
+ stage: "parseIndexTable"
1099
+ });
1100
+ }
1101
+ const bytes = new Int32Array(indexBuffer.subarray(pointer, pointer + recordSize));
1102
+ bytes.reverse();
1103
+ const mask = bytes.reduce((bits, byte) => bits + ("00000000" + byte.toString(2)).slice(-8), "").split("").reverse().map((bit) => parseInt(bit));
1104
+ const symbolIndices = [];
1105
+ fields.forEach((field) => {
1106
+ const bitOffset = parseInt(field["BitOffset"], 10);
1107
+ const bitWidth = parseInt(field["BitWidth"], 10);
1108
+ const bias = parseInt(field["Bias"], 10);
1109
+ let symbolIndex;
1110
+ if (bitWidth === 0) {
1111
+ symbolIndex = 0;
1112
+ } else {
1113
+ symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
1114
+ }
1115
+ symbolIndex += bias;
1116
+ symbolIndices.push(symbolIndex);
1117
+ });
1118
+ this._indexTable.push(symbolIndices);
1119
+ }
1120
+ }
1121
+ /**
1122
+ * Loads the QVD file into memory and parses it.
1123
+ *
1124
+ * @param {number|null} maxRows The maximum number of rows to load. If null, all rows are loaded.
1125
+ * @return {Promise<QvdDataFrame>} The loaded QVD file.
1126
+ */
1127
+ async load(maxRows = null) {
1128
+ await this._readData(maxRows);
1129
+ await this._parseHeader();
1130
+ await this._parseSymbolTable();
1131
+ await this._parseIndexTable(maxRows);
1132
+ assert__default.default(this._header, "The QVD file header has not been parsed.");
1133
+ assert__default.default(this._symbolTable, "The QVD file symbol table has not been parsed.");
1134
+ assert__default.default(this._indexTable, "The QVD file index table has not been parsed.");
1135
+ const getRow = (index) => {
1136
+ if (!this._indexTable || index >= this._indexTable.length) {
1137
+ throw new exports.QvdValidationError("Row index out of bounds", {
1138
+ index,
1139
+ max: this._indexTable ? this._indexTable.length - 1 : -1,
1140
+ file: this._path
1141
+ });
1142
+ }
1143
+ return this._indexTable?.[index].map((symbolIndex, fieldIndex) => {
1144
+ if (symbolIndex < 0) {
1145
+ return null;
1146
+ }
1147
+ const symbol = this._symbolTable?.[fieldIndex]?.[symbolIndex];
1148
+ const value = symbol?.toPrimaryValue();
1149
+ if (typeof value === "string") {
1150
+ if (!isNaN(Number(value))) {
1151
+ return Number(value);
1152
+ }
1153
+ }
1154
+ return value;
1155
+ });
1156
+ };
1157
+ let fields = this._header["QvdTableHeader"]["Fields"]["QvdFieldHeader"];
1158
+ if (!Array.isArray(fields)) {
1159
+ fields = [fields];
1160
+ }
1161
+ const columns = fields.map((field) => field["FieldName"]);
1162
+ const data = this._indexTable.map((_, index) => getRow(index));
1163
+ const metadata = this._header["QvdTableHeader"];
1164
+ return new exports.QvdDataFrame(data, columns, metadata);
1165
+ }
1166
+ };
1167
+ }
1168
+ });
1169
+
1170
+ // src/QvdDataFrame.js
1171
+ exports.QvdDataFrame = void 0;
1172
+ var init_QvdDataFrame = __esm({
1173
+ "src/QvdDataFrame.js"() {
1174
+ init_QvdErrors();
1175
+ exports.QvdDataFrame = class _QvdDataFrame {
1176
+ /**
1177
+ * Represents the data frame stored inside a QVD file.
1178
+ * @param {Array<Array<any>>} data The data of the data frame.
1179
+ * @param {Array<string>} columns The columns of the data frame.
1180
+ * @param {QvdMetadata|null} metadata The metadata from the QVD file header (optional).
1181
+ */
1182
+ constructor(data, columns, metadata = null) {
1183
+ this._data = data;
1184
+ this._columns = columns;
1185
+ this._metadata = metadata;
1186
+ }
1187
+ /**
1188
+ * Returns the data of the data frame.
1189
+ */
1190
+ get data() {
1191
+ return this._data;
1192
+ }
1193
+ /**
1194
+ * Returns the columns of the data frame.
1195
+ */
1196
+ get columns() {
1197
+ return this._columns;
1198
+ }
1199
+ /**
1200
+ * Returns the shape of the data frame.
1201
+ */
1202
+ get shape() {
1203
+ return [this._data.length, this._columns.length];
1204
+ }
1205
+ /**
1206
+ * Returns the complete metadata object from the QVD file header.
1207
+ * @return {Object|null} The complete metadata object or null if not available.
1208
+ */
1209
+ get metadata() {
1210
+ return this._metadata;
1211
+ }
1212
+ /**
1213
+ * Returns file-level metadata from the QVD header.
1214
+ * @return {Object} File-level metadata properties.
1215
+ */
1216
+ get fileMetadata() {
1217
+ if (!this._metadata) {
1218
+ return {};
1219
+ }
1220
+ const header = this._metadata;
1221
+ return {
1222
+ qvBuildNo: header.QvBuildNo,
1223
+ creatorDoc: header.CreatorDoc,
1224
+ createUtcTime: header.CreateUtcTime,
1225
+ sourceCreateUtcTime: header.SourceCreateUtcTime,
1226
+ sourceFileUtcTime: header.SourceFileUtcTime,
1227
+ sourceFileSize: header.SourceFileSize,
1228
+ staleUtcTime: header.StaleUtcTime,
1229
+ tableName: header.TableName,
1230
+ noOfRecords: header.NoOfRecords,
1231
+ recordByteSize: header.RecordByteSize,
1232
+ offset: header.Offset,
1233
+ length: header.Length,
1234
+ compression: header.Compression,
1235
+ comment: header.Comment,
1236
+ encryptionInfo: header.EncryptionInfo,
1237
+ tableTags: header.TableTags,
1238
+ profilingData: header.ProfilingData,
1239
+ lineage: header.Lineage
1240
+ };
1241
+ }
1242
+ /**
1243
+ * Returns field-level metadata for a specific field/column.
1244
+ * @param {string} fieldName The name of the field.
1245
+ * @return {Object|null} Field metadata or null if field not found.
1246
+ */
1247
+ getFieldMetadata(fieldName) {
1248
+ if (!this._metadata || !this._metadata.Fields || !this._metadata.Fields.QvdFieldHeader) {
1249
+ return null;
1250
+ }
1251
+ let fields = this._metadata.Fields.QvdFieldHeader;
1252
+ if (!Array.isArray(fields)) {
1253
+ fields = [fields];
1254
+ }
1255
+ const field = fields.find((f) => f.FieldName === fieldName);
1256
+ if (!field) {
1257
+ return null;
1258
+ }
1259
+ return {
1260
+ fieldName: field.FieldName,
1261
+ bitOffset: field.BitOffset,
1262
+ bitWidth: field.BitWidth,
1263
+ bias: field.Bias,
1264
+ noOfSymbols: field.NoOfSymbols,
1265
+ offset: field.Offset,
1266
+ length: field.Length,
1267
+ comment: field.Comment,
1268
+ numberFormat: field.NumberFormat,
1269
+ tags: field.Tags
1270
+ };
1271
+ }
1272
+ /**
1273
+ * Returns field-level metadata for all fields.
1274
+ * @return {Array<Object>} Array of field metadata objects.
1275
+ */
1276
+ getAllFieldMetadata() {
1277
+ if (!this._metadata || !this._metadata.Fields || !this._metadata.Fields.QvdFieldHeader) {
1278
+ return [];
1279
+ }
1280
+ let fields = this._metadata.Fields.QvdFieldHeader;
1281
+ if (!Array.isArray(fields)) {
1282
+ fields = [fields];
1283
+ }
1284
+ return fields.map((field) => ({
1285
+ fieldName: field.FieldName,
1286
+ bitOffset: field.BitOffset,
1287
+ bitWidth: field.BitWidth,
1288
+ bias: field.Bias,
1289
+ noOfSymbols: field.NoOfSymbols,
1290
+ offset: field.Offset,
1291
+ length: field.Length,
1292
+ comment: field.Comment,
1293
+ numberFormat: field.NumberFormat,
1294
+ tags: field.Tags
1295
+ }));
1296
+ }
1297
+ /**
1298
+ * @typedef {Object} FileMetadataUpdate
1299
+ * @property {string|number} [qvBuildNo] - QlikView build number
1300
+ * @property {string} [creatorDoc] - Creator document
1301
+ * @property {string} [createUtcTime] - Creation UTC time
1302
+ * @property {string} [sourceCreateUtcTime] - Source creation UTC time
1303
+ * @property {string} [sourceFileUtcTime] - Source file UTC time
1304
+ * @property {string|number} [sourceFileSize] - Source file size
1305
+ * @property {string} [staleUtcTime] - Stale UTC time
1306
+ * @property {string} [tableName] - Table name
1307
+ * @property {string} [compression] - Compression type
1308
+ * @property {string} [comment] - Comment
1309
+ * @property {string} [encryptionInfo] - Encryption info
1310
+ * @property {string} [tableTags] - Table tags
1311
+ * @property {string} [profilingData] - Profiling data
1312
+ * @property {Object|string} [lineage] - Lineage
1313
+ */
1314
+ /**
1315
+ * Sets modifiable file-level metadata. Immutable properties related to data storage are ignored.
1316
+ * @param {FileMetadataUpdate} metadata Object containing metadata properties to update.
1317
+ */
1318
+ setFileMetadata(metadata) {
1319
+ if (!this._metadata) {
1320
+ this._metadata = {
1321
+ QvBuildNo: 50667,
1322
+ CreatorDoc: "",
1323
+ CreateUtcTime: "",
1324
+ SourceCreateUtcTime: "",
1325
+ SourceFileUtcTime: "",
1326
+ SourceFileSize: -1,
1327
+ StaleUtcTime: "",
1328
+ TableName: "",
1329
+ Fields: {
1330
+ QvdFieldHeader: this._columns.map((column) => ({
1331
+ FieldName: column,
1332
+ BitOffset: 0,
1333
+ BitWidth: 0,
1334
+ Bias: 0,
1335
+ NoOfSymbols: 0,
1336
+ Offset: 0,
1337
+ Length: 0,
1338
+ Comment: "",
1339
+ NumberFormat: {
1340
+ Type: "UNKNOWN",
1341
+ nDec: "0",
1342
+ UseThou: "0",
1343
+ Fmt: "",
1344
+ Dec: "",
1345
+ Thou: ""
1346
+ },
1347
+ Tags: {}
1348
+ }))
1349
+ },
1350
+ NoOfRecords: 0,
1351
+ RecordByteSize: 0,
1352
+ Offset: 0,
1353
+ Length: 0,
1354
+ Compression: "",
1355
+ Comment: "",
1356
+ EncryptionInfo: "",
1357
+ TableTags: "",
1358
+ ProfilingData: "",
1359
+ Lineage: {}
1360
+ };
1361
+ }
1362
+ const modifiableFields = [
1363
+ "qvBuildNo",
1364
+ "creatorDoc",
1365
+ "createUtcTime",
1366
+ "sourceCreateUtcTime",
1367
+ "sourceFileUtcTime",
1368
+ "sourceFileSize",
1369
+ "staleUtcTime",
1370
+ "tableName",
1371
+ "compression",
1372
+ "comment",
1373
+ "encryptionInfo",
1374
+ "tableTags",
1375
+ "profilingData",
1376
+ "lineage"
1377
+ ];
1378
+ const fieldMapping = {
1379
+ qvBuildNo: "QvBuildNo",
1380
+ creatorDoc: "CreatorDoc",
1381
+ createUtcTime: "CreateUtcTime",
1382
+ sourceCreateUtcTime: "SourceCreateUtcTime",
1383
+ sourceFileUtcTime: "SourceFileUtcTime",
1384
+ sourceFileSize: "SourceFileSize",
1385
+ staleUtcTime: "StaleUtcTime",
1386
+ tableName: "TableName",
1387
+ compression: "Compression",
1388
+ comment: "Comment",
1389
+ encryptionInfo: "EncryptionInfo",
1390
+ tableTags: "TableTags",
1391
+ profilingData: "ProfilingData",
1392
+ lineage: "Lineage"
1393
+ };
1394
+ modifiableFields.forEach((field) => {
1395
+ if (metadata[field] !== void 0) {
1396
+ this._metadata[fieldMapping[field]] = metadata[field];
1397
+ }
1398
+ });
1399
+ }
1400
+ /**
1401
+ * @typedef {Object} FieldMetadataUpdate
1402
+ * @property {string} [comment] - Field comment
1403
+ * @property {QvdNumberFormat|string} [numberFormat] - Number format
1404
+ * @property {Object|string} [tags] - Field tags
1405
+ */
1406
+ /**
1407
+ * Sets modifiable field-level metadata for a specific field.
1408
+ * Immutable properties related to data storage (Offset, Length, BitOffset, etc.) are ignored.
1409
+ * @param {string} fieldName The name of the field.
1410
+ * @param {FieldMetadataUpdate} metadata Object containing field metadata properties to update.
1411
+ */
1412
+ setFieldMetadata(fieldName, metadata) {
1413
+ if (!this._metadata || !this._metadata.Fields || !this._metadata.Fields.QvdFieldHeader) {
1414
+ return;
1415
+ }
1416
+ let fields = this._metadata.Fields.QvdFieldHeader;
1417
+ if (!Array.isArray(fields)) {
1418
+ fields = [fields];
1419
+ this._metadata.Fields.QvdFieldHeader = fields;
1420
+ }
1421
+ const fieldIndex = fields.findIndex((f) => f.FieldName === fieldName);
1422
+ if (fieldIndex === -1) {
1423
+ return;
1424
+ }
1425
+ if (metadata.comment !== void 0) {
1426
+ fields[fieldIndex].Comment = metadata.comment;
1427
+ }
1428
+ if (metadata.numberFormat !== void 0) {
1429
+ fields[fieldIndex].NumberFormat = metadata.numberFormat;
1430
+ }
1431
+ if (metadata.tags !== void 0) {
1432
+ fields[fieldIndex].Tags = metadata.tags;
1433
+ }
1434
+ }
1435
+ /**
1436
+ * Returns the first n rows of the data frame.
1437
+ *
1438
+ * @param {number} n The number of rows to return.
1439
+ * @return {QvdDataFrame} The first n rows of the data frame.
1440
+ * @throws {QvdValidationError} If n is not a non-negative integer.
1441
+ */
1442
+ head(n = 5) {
1443
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
1444
+ throw new exports.QvdValidationError("head() requires a non-negative integer", {
1445
+ provided: n,
1446
+ type: typeof n
1447
+ });
1448
+ }
1449
+ return new _QvdDataFrame(this._data.slice(0, n), this._columns, this._metadata);
1450
+ }
1451
+ /**
1452
+ * Returns the last n rows of the data frame.
1453
+ *
1454
+ * @param {number} n The number of rows to return.
1455
+ * @return {QvdDataFrame} The first n rows of the data frame.
1456
+ * @throws {QvdValidationError} If n is not a non-negative integer.
1457
+ */
1458
+ tail(n = 5) {
1459
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
1460
+ throw new exports.QvdValidationError("tail() requires a non-negative integer", {
1461
+ provided: n,
1462
+ type: typeof n
1463
+ });
1464
+ }
1465
+ return new _QvdDataFrame(n === 0 ? [] : this._data.slice(-n), this._columns, this._metadata);
1466
+ }
1467
+ /**
1468
+ * Returns the selected rows of the data frame.
1469
+ *
1470
+ * @param {...number} args The indices of the rows to return.
1471
+ * @return {QvdDataFrame} The selected rows of the data frame.
1472
+ * @throws {QvdValidationError} If any index is not an integer or is out of bounds.
1473
+ */
1474
+ rows(...args) {
1475
+ for (const index of args) {
1476
+ if (typeof index !== "number" || !Number.isInteger(index)) {
1477
+ throw new exports.QvdValidationError("rows() requires integer indices", {
1478
+ provided: index,
1479
+ type: typeof index
1480
+ });
1481
+ }
1482
+ if (index < 0 || index >= this._data.length) {
1483
+ throw new exports.QvdValidationError(`Row index ${index} out of bounds`, {
1484
+ index,
1485
+ validRange: [0, this._data.length - 1],
1486
+ dataLength: this._data.length
1487
+ });
1488
+ }
1489
+ }
1490
+ return new _QvdDataFrame(
1491
+ args.map((index) => this._data[index]),
1492
+ this._columns,
1493
+ this._metadata
1494
+ );
1495
+ }
1496
+ /**
1497
+ * Returns the value at the specified row and column.
1498
+ *
1499
+ * @param {number} row The index of the row.
1500
+ * @param {string} column The name of the column.
1501
+ * @return {any} The value at the specified row and column.
1502
+ * @throws {QvdValidationError} If row is not an integer, out of bounds, or column does not exist.
1503
+ */
1504
+ at(row, column) {
1505
+ if (typeof row !== "number" || !Number.isInteger(row)) {
1506
+ throw new exports.QvdValidationError("Row index must be an integer", {
1507
+ provided: row,
1508
+ type: typeof row
1509
+ });
1510
+ }
1511
+ if (row < 0 || row >= this._data.length) {
1512
+ throw new exports.QvdValidationError(`Row index ${row} out of bounds`, {
1513
+ index: row,
1514
+ validRange: [0, this._data.length - 1],
1515
+ dataLength: this._data.length
1516
+ });
1517
+ }
1518
+ if (!this._columns.includes(column)) {
1519
+ throw new exports.QvdValidationError(`Column '${column}' does not exist`, {
1520
+ column,
1521
+ availableColumns: this._columns
1522
+ });
1523
+ }
1524
+ return this._data[row][this._columns.indexOf(column)];
1525
+ }
1526
+ /**
1527
+ * Selects the specified columns from the data frame.
1528
+ *
1529
+ * @param {...string} args The names of the columns to select.
1530
+ * @return {QvdDataFrame} The selected columns of the data frame.
1531
+ * @throws {QvdValidationError} If any column name does not exist.
1532
+ */
1533
+ select(...args) {
1534
+ for (const column of args) {
1535
+ if (!this._columns.includes(column)) {
1536
+ throw new exports.QvdValidationError(`Column '${column}' does not exist`, {
1537
+ column,
1538
+ availableColumns: this._columns
1539
+ });
1540
+ }
1541
+ }
1542
+ const indices = args.map((arg) => this._columns.indexOf(arg));
1543
+ const data = this._data.map((row) => indices.map((index) => row[index]));
1544
+ const columns = indices.map((index) => this._columns[index]);
1545
+ return new _QvdDataFrame(data, columns, this._metadata);
1546
+ }
1547
+ /**
1548
+ * Returns the data frame as a dictionary.
1549
+ *
1550
+ * @return {Promise<{columns: Array<string>, data: Array<Array<any>>}>} The data frame as a dictionary.
1551
+ */
1552
+ async toDict() {
1553
+ return { columns: this._columns, data: this._data };
1554
+ }
1555
+ /**
1556
+ * Persists the data frame to a QVD file.
1557
+ *
1558
+ * @param {string} path The path to the QVD file.
1559
+ * @param {Object} [options] Optional writing options.
1560
+ * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path must be within this directory.
1561
+ * @param {Function} [options.onProgress] Optional progress callback function that receives progress updates during write operations.
1562
+ */
1563
+ async toQvd(path3, options = {}) {
1564
+ const { QvdFileWriter: QvdFileWriter2 } = await Promise.resolve().then(() => (init_QvdFileWriter(), QvdFileWriter_exports));
1565
+ const writerOptions = {
1566
+ allowedDir: options.allowedDir,
1567
+ onProgress: options.onProgress
1568
+ };
1569
+ await new QvdFileWriter2(path3, this, writerOptions).save();
1570
+ }
1571
+ /**
1572
+ * Loads a QVD file and returns its data frame.
1573
+ *
1574
+ * @param {string} path The path to the QVD file.
1575
+ * @param {Object} [options] Optional loading options.
1576
+ * @param {number|null} [options.maxRows] The maximum number of rows to load. If not specified, all rows are loaded.
1577
+ * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path must be within this directory.
1578
+ * @return {Promise<QvdDataFrame>} The data frame of the QVD file.
1579
+ */
1580
+ static async fromQvd(path3, options = {}) {
1581
+ const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
1582
+ const readerOptions = { allowedDir: options.allowedDir };
1583
+ return await new QvdFileReader2(path3, readerOptions).load(options.maxRows !== void 0 ? options.maxRows : null);
1584
+ }
1585
+ /**
1586
+ * Constructs a data frame from a dictionary.
1587
+ *
1588
+ * @param {{columns: Array<string>, data: Array<Array<any>>}} data The dictionary to construct the data frame from.
1589
+ * @return {Promise<QvdDataFrame>} The constructed data frame.
1590
+ */
1591
+ static async fromDict(data) {
1592
+ if (!data.columns) {
1593
+ throw new exports.QvdValidationError("The dictionary to construct the data frame from does not contain any columns.", {
1594
+ data
1595
+ });
1596
+ }
1597
+ if (!data.data) {
1598
+ throw new exports.QvdValidationError("The dictionary to construct the data frame from does not contain any data.", {
1599
+ data
1600
+ });
1601
+ }
1602
+ return new _QvdDataFrame(data.data, data.columns);
1603
+ }
1604
+ };
1605
+ }
1606
+ });
1607
+
1608
+ // src/index.js
1609
+ init_QvdSymbol();
1610
+ init_QvdDataFrame();
1611
+ init_QvdFileReader();
1612
+ init_QvdFileWriter();
1613
+ init_QvdErrors();
1614
+ //# sourceMappingURL=index.cjs.map
1615
+ //# sourceMappingURL=index.cjs.map