diff --git a/demo/node/rntuple.js b/demo/node/rntuple.js index 5dc470ec2..fe2d0f3f7 100644 --- a/demo/node/rntuple.js +++ b/demo/node/rntuple.js @@ -61,4 +61,4 @@ else console.log('test 3 - readString passed'); else console.error('FAILURE: test 3 - readString does not match'); -} +} \ No newline at end of file diff --git a/demo/node/rntuple_test.js b/demo/node/rntuple_test.js index 3712eb199..01993a23d 100644 --- a/demo/node/rntuple_test.js +++ b/demo/node/rntuple_test.js @@ -18,13 +18,9 @@ if (rntuple.builder?.name !== 'Staff') else console.log('OK: name is', rntuple.builder?.name); -if (typeof rntuple.builder?.version !== 'number') - console.error('FAILURE: version is missing or invalid'); -else - console.log('OK: version is', rntuple.builder.version); -if (!rntuple.builder?.description) - console.error('FAILURE: description is missing'); +if (rntuple.builder?.description !== '') + console.error('FAILURE: description should be the empty string'); else console.log('OK: description is', rntuple.builder.description); @@ -33,3 +29,17 @@ if (rntuple.builder?.xxhash3 === undefined || rntuple.builder.xxhash3 === null) else console.log('OK: xxhash3 is', '0x' + rntuple.builder.xxhash3.toString(16).padStart(16, '0')); +// Fields Check + +if (!rntuple.builder?.fieldDescriptors?.length) + console.error('FAILURE: No fields deserialized'); +else { + console.log(`OK: ${rntuple.builder.fieldDescriptors.length} field(s) deserialized`); + for (let i = 0; i < rntuple.builder.fieldDescriptors.length; ++i) { + const field = rntuple.builder.fieldDescriptors[i]; + if (!field.fieldName || !field.typeName) + console.error(`FAILURE: Field ${i} is missing name or type`); + else + console.log(`OK: Field ${i}: ${field.fieldName} (${field.typeName})`); + } +} diff --git a/modules/rntuple.mjs b/modules/rntuple.mjs index d2cde094c..abf87a004 100644 --- a/modules/rntuple.mjs +++ b/modules/rntuple.mjs @@ -3,15 +3,17 @@ const LITTLE_ENDIAN = true; class RBufferReader { constructor(buffer) { - if (buffer instanceof ArrayBuffer) - this.buffer = buffer; - else if (ArrayBuffer.isView(buffer)) { - const bytes = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); - this.buffer = bytes.slice().buffer; + if (buffer instanceof ArrayBuffer) { + this.buffer = buffer; + this.byteOffset = 0; + this.byteLength = buffer.byteLength; + } else if (ArrayBuffer.isView(buffer)) { + this.buffer = buffer.buffer; + this.byteOffset = buffer.byteOffset; + this.byteLength = buffer.byteLength; } else throw new TypeError('Invalid buffer type'); - this.view = new DataView(this.buffer); this.offset = 0; } @@ -23,7 +25,7 @@ class RBufferReader { // Read unsigned 8-bit integer (1 BYTE) readU8() { - const val = this.view.getUint8(this.offset, LITTLE_ENDIAN); + const val = this.view.getUint8(this.offset); this.offset += 1; return val; } @@ -44,7 +46,7 @@ class RBufferReader { // Read signed 8-bit integer (1 BYTE) readS8() { - const val = this.view.getInt8(this.offset, LITTLE_ENDIAN); + const val = this.view.getInt8(this.offset); this.offset += 1; return val; } @@ -70,6 +72,13 @@ class RBufferReader { return val; } + // Read 64-bit float (8 BYTES) + readF64() { + const val = this.view.getFloat64(this.offset, LITTLE_ENDIAN); + this.offset += 8; + return val; + } + // Read a string with 32-bit length prefix readString() { const length = this.readU32(); @@ -98,36 +107,28 @@ class RBufferReader { class RNTupleDescriptorBuilder { - deserializeHeader(header_blob) { +deserializeHeader(header_blob) { if (!header_blob) return; - const reader = new RBufferReader(header_blob); + const reader = new RBufferReader(header_blob); + // Read the envelope metadata + this._readEnvelopeMetadata(reader); - // 1. Read header version (4 bytes) - this.version = reader.readU32(); + // TODO: Validate the envelope checksum at the end of deserialization + // const payloadStart = reader.offset; - // 2. Read feature flags (4 bytes) - this.headerFeatureFlags = reader.readU32(); + // Read feature flags list (may span multiple 64-bit words) + this._readFeatureFlags(reader); - // 3. Read xxhash3 (64-bit, 8 bytes) - this.xxhash3 = reader.readU64(); + // Read metadata strings + this.name = reader.readString(); + this.description = reader.readString(); + this.writer = reader.readString(); - // 4. Read name (length-prefixed string) - this.name = reader.readString(); - - // 5. Read description (length-prefixed string) - this.description = reader.readString(); - - - // Console output to verify deserialization results - console.log('Version:', this.version); - console.log('Header Feature Flags:', this.headerFeatureFlags); - console.log('xxhash3:', '0x' + this.xxhash3.toString(16).padStart(16, '0')); - console.log('Name:', this.name); - console.log('Description:', this.description); + // List frame: list of field record frames + this._readFieldDescriptors(reader); } - deserializeFooter(footer_blob) { if (!footer_blob) return; @@ -141,6 +142,83 @@ deserializeFooter(footer_blob) { } +_readEnvelopeMetadata(reader) { + const typeAndLength = reader.readU64(), + + // Envelope metadata + // The 16 bits are the envelope type ID, and the 48 bits are the envelope length + envelopeType = Number(typeAndLength & 0xFFFFn), + envelopeLength = Number((typeAndLength >> 16n) & 0xFFFFFFFFFFFFn); + + console.log('Envelope Type ID:', envelopeType); + console.log('Envelope Length:', envelopeLength); + return { envelopeType, envelopeLength }; +} + +_readFeatureFlags(reader) { + this.featureFlags = []; + while (true) { + const val = reader.readU64(); + this.featureFlags.push(val); + if ((val & 0x8000000000000000n) === 0n) break; // MSB not set: end of list + } + + // verify all feature flags are zero + if (this.featureFlags.some(v => v !== 0n)) + throw new Error('Unexpected non-zero feature flags: ' + this.featureFlags); +} + +_readFieldDescriptors(reader) { +const fieldListSize = reader.readS64(), // signed 64-bit +fieldListIsList = fieldListSize < 0; + + + if (!fieldListIsList) + throw new Error('Field list frame is not a list frame, which is required.'); + + const fieldListCount = reader.readU32(); // number of field entries + console.log('Field List Count:', fieldListCount); + + // List frame: list of field record frames + + const fieldDescriptors = []; + for (let i = 0; i < fieldListCount; ++i) { + const fieldRecordSize = reader.readS64(), + fieldVersion = reader.readU32(), + typeVersion = reader.readU32(), + parentFieldId = reader.readU32(), + structRole = reader.readU16(), + flags = reader.readU16(), + + fieldName = reader.readString(), + typeName = reader.readString(), + typeAlias = reader.readString(), + description = reader.readString(); + console.log(`Field Record Size: ${fieldRecordSize}`); + let arraySize = null, sourceFieldId = null, checksum = null; + + if (flags & 0x1) arraySize = reader.readU64(); + if (flags & 0x2) sourceFieldId = reader.readU32(); + if (flags & 0x4) checksum = reader.readU32(); + + fieldDescriptors.push({ + fieldVersion, + typeVersion, + parentFieldId, + structRole, + flags, + fieldName, + typeName, + typeAlias, + description, + arraySize, + sourceFieldId, + checksum + }); +} + this.fieldDescriptors = fieldDescriptors; +} + } /** @summary Very preliminary function to read header/footer from RNTuple