diff --git a/README.md b/README.md index 4ad1e453..c1d23471 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ The following resources were helpful during creation of this library: - [MIPAMS JPEG Systems](https://github.com/nickft/mipams-jpeg-systems) - [cbor-x](https://github.com/kriszyp/cbor-x) - [mocha](https://mochajs.org) +- [typed-binary](https://github.com/iwoplaza/typed-binary) Thank you for providing them and keeping open source alive! diff --git a/package-lock.json b/package-lock.json index 260762a6..8f583302 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@peculiar/x509": "^1.11.0", "cbor-x": "^1.6.0", "crc-32": "^1.2.2", - "pkijs": "^3.2.4" + "pkijs": "^3.2.4", + "typed-binary": "^4.0.1" }, "devDependencies": { "@eslint/js": "^9.8.0", @@ -4308,6 +4309,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-binary": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/typed-binary/-/typed-binary-4.0.1.tgz", + "integrity": "sha512-nne2iMvXX8Qi3KI7do55udYsPNIUr7zFhPJNdWpujkWUzr6sJK6gQjCUcZGMM6+lws75oiFtGk8if6S2KgWppg==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", diff --git a/package.json b/package.json index e7c572d8..945617c7 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "@peculiar/x509": "^1.11.0", "cbor-x": "^1.6.0", "crc-32": "^1.2.2", - "pkijs": "^3.2.4" + "pkijs": "^3.2.4", + "typed-binary": "^4.0.1" }, "mocha": { "require": [ diff --git a/src/jumbf/Box.ts b/src/jumbf/Box.ts index beadd61a..903f9517 100644 --- a/src/jumbf/Box.ts +++ b/src/jumbf/Box.ts @@ -1,15 +1,15 @@ +import * as bin from 'typed-binary'; import { IBox } from './IBox'; export class Box implements IBox { public readonly type: string; + public readonly schema: bin.ISchema; - constructor(type: string) { + constructor(type: string, schema: bin.ISchema) { this.type = type; + this.schema = schema; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function - public parse(buf: Uint8Array) {} - public toString(prefix?: string | undefined) { return `${prefix ?? ''}${this.type}`; } diff --git a/src/jumbf/BoxReader.ts b/src/jumbf/BoxReader.ts index b8799ac2..96e19215 100644 --- a/src/jumbf/BoxReader.ts +++ b/src/jumbf/BoxReader.ts @@ -1,60 +1,12 @@ -import { BinaryHelper } from '../util'; -import { Box } from './Box'; -import { C2PASaltBox } from './C2PASaltBox'; -import { CBORBox } from './CBORBox'; -import { CodestreamBox } from './CodestreamBox'; -import { DescriptionBox } from './DescriptionBox'; -import { EmbeddedFileBox } from './EmbeddedFileBox'; -import { EmbeddedFileDescriptionBox } from './EmbeddedFileDescriptionBox'; -import { IBox } from './IBox'; -import { JSONBox } from './JSONBox'; -import { SuperBox } from './SuperBox'; -import { UUIDBox } from './UUIDBox'; +import * as bin from 'typed-binary'; +import { GenericBoxSchema } from './GenericBoxSchema'; export class BoxReader { - private static readonly HEADER_LENGTH = 8; + private static readonly schema = new GenericBoxSchema(); + public static readFromBuffer(buf: Uint8Array) { + const reader = new bin.BufferReader(buf, { endianness: 'big' }); + const box = BoxReader.schema.read(reader); - public static readFromBuffer(buf: Uint8Array, urlPrefix?: string) { - if (buf.length < this.HEADER_LENGTH) { - throw new Error('JUMBFBox: Data too short'); - } - - // LBox: Box length including LBox itself - const lBox = BinaryHelper.readUInt32(buf, 0); - if (lBox > buf.length || lBox < 8) { - // There are special (low) values for LBox but we don't support them - throw new Error('JUMBFBox: Invalid box length'); - } - - const tBox = BinaryHelper.readString(buf, 4, 4); - const box = this.createBox(tBox); - - box.parse(buf.subarray(this.HEADER_LENGTH, lBox), urlPrefix); - return { box, lBox }; - } - - private static createBox(boxType: string): IBox { - switch (boxType) { - case SuperBox.typeCode: - return new SuperBox(); - case DescriptionBox.typeCode: - return new DescriptionBox(); - case C2PASaltBox.typeCode: - return new C2PASaltBox(); - case CBORBox.typeCode: - return new CBORBox(); - case CodestreamBox.typeCode: - return new CodestreamBox(); - case EmbeddedFileBox.typeCode: - return new EmbeddedFileBox(); - case EmbeddedFileDescriptionBox.typeCode: - return new EmbeddedFileDescriptionBox(); - case JSONBox.typeCode: - return new JSONBox(); - case UUIDBox.typeCode: - return new UUIDBox(); - default: - return new Box(boxType); - } + return { box, lBox: reader.currentByteOffset }; } } diff --git a/src/jumbf/BoxSchema.ts b/src/jumbf/BoxSchema.ts new file mode 100644 index 00000000..6ae3338c --- /dev/null +++ b/src/jumbf/BoxSchema.ts @@ -0,0 +1,44 @@ +import * as bin from 'typed-binary'; +import { IBox } from './IBox'; +import * as schemata from './schemata'; + +/** + * Intermediate abstract class for JUMBF box schemata + * + * In order to implement a schema for a concrete box, implement + * the three abstract methods. Their implementations should follow + * the expected code for a `bin.ISchema`. The only difference + * is when reading, wher the length and type are supplied as + * additional parameters. + */ +export abstract class BoxSchema extends bin.Schema { + readonly length = schemata.length; + readonly type = schemata.type; + + public read(input: bin.ISerialInput): bin.Parsed { + const length = this.length.read(input); + const type = this.type.read(input); + return this.readContent(input, type, length); + } + + public write(output: bin.ISerialOutput, value: bin.Parsed): void { + const length = this.measure(value).size; + this.length.write(output, length); + this.type.write(output, value.type); + this.writeContent(output, value); + } + + public measure( + value: bin.Parsed | bin.MaxValue, + measurer: bin.IMeasurer = new bin.Measurer(), + ): bin.IMeasurer { + return this.measureContent(value, measurer).add( + 4 + // length + 4, // type + ); + } + + abstract readContent(input: bin.ISerialInput, type: string, length: number): bin.Parsed; + abstract writeContent(output: bin.ISerialOutput, value: bin.Parsed): void; + abstract measureContent(value: bin.Parsed | bin.MaxValue, measurer: bin.IMeasurer): bin.IMeasurer; +} diff --git a/src/jumbf/C2PASaltBox.ts b/src/jumbf/C2PASaltBox.ts index f86b793b..f1ea93c2 100644 --- a/src/jumbf/C2PASaltBox.ts +++ b/src/jumbf/C2PASaltBox.ts @@ -1,17 +1,40 @@ +import * as bin from 'typed-binary'; import { BinaryHelper } from '../util'; import { Box } from './Box'; +import { BoxSchema } from './BoxSchema'; + +class C2PASaltBoxSchema extends BoxSchema { + readContent(input: bin.ISerialInput, type: string, length: number): C2PASaltBox { + if (type != C2PASaltBox.typeCode) throw new Error(`C2PASaltBox: Unexpected type ${type}`); + if (length !== 8 + 16 && length !== 8 + 32) throw new Error(`C2PASaltBox: Unexpected length ${length}`); + + const salt = []; + for (let i = 8; i != length; i++) { + salt.push(input.readByte()); + } + + const box = new C2PASaltBox(); + box.salt = new Uint8Array(salt); + + return box; + } + + writeContent(output: bin.ISerialOutput, value: C2PASaltBox): void { + value.salt?.forEach(byte => output.writeByte(byte)); + } + + measureContent(value: C2PASaltBox, measurer: bin.IMeasurer): bin.IMeasurer { + return measurer.add(value.salt ? value.salt.length : 0); + } +} export class C2PASaltBox extends Box { public static readonly typeCode = 'c2sh'; + public static readonly schema = new C2PASaltBoxSchema(); public salt?: Uint8Array; constructor() { - super(C2PASaltBox.typeCode); - } - - public parse(buf: Uint8Array) { - if (buf.length !== 16 && buf.length !== 32) throw new Error('C2PASaltBox: Invalid length'); - this.salt = buf; + super(C2PASaltBox.typeCode, C2PASaltBox.schema); } public toString(prefix?: string | undefined): string { diff --git a/src/jumbf/CBORBox.ts b/src/jumbf/CBORBox.ts index b0db9eed..0a34e828 100644 --- a/src/jumbf/CBORBox.ts +++ b/src/jumbf/CBORBox.ts @@ -1,28 +1,82 @@ import * as cbor from 'cbor-x'; +import * as bin from 'typed-binary'; import { Box } from './Box'; +import { BoxSchema } from './BoxSchema'; -export class CBORBox extends Box { - public static readonly typeCode = 'cbor'; - public content: unknown; - public rawContent: Uint8Array | undefined; +/** + * Schema for CBOR boxes + * + * Note: A full round-trip during encoding and decoding is not always + * possible, because there are sometimes multiple representations for + * the same data. Try e.g. decoding the byte sequences [a1 61 61 01] + * and [b9 00 01 61 61 01] in https://cbor.me, they both represent + * the same data. + */ +class CBORBoxSchema extends BoxSchema { + readContent(input: bin.ISerialInput, type: string, length: number): CBORBox { + if (type != CBORBox.typeCode) throw new Error(`CBORBox: Unexpected type ${type}`); - constructor() { - super(CBORBox.typeCode); - } + const data = []; + for (let i = 0; i < length - 8; i++) { + data.push(input.readByte()); + } - public parse(buf: Uint8Array) { - this.rawContent = buf; + const box = new CBORBox(); + box.rawContent = new Uint8Array(data); try { - this.content = cbor.decode(buf); - - // Ignore unknown CBOR tags - if (this.content instanceof cbor.Tag) { - this.content = this.content.value; + // If the data is tagged, store content and tag separately, + // but ignore the tag otherwise. + const decoded: unknown = cbor.decode(box.rawContent); + if (decoded instanceof cbor.Tag) { + box.tag = decoded.tag; + box.content = decoded.value; + } else { + box.tag = undefined; + box.content = decoded; } } catch { // TODO This needs to be properly reported as a validation error throw new Error('CBORBox: Invalid CBOR data'); } + + return box; + } + + writeContent(output: bin.ISerialOutput, value: CBORBox): void { + if (!value.rawContent) { + if (value.tag !== undefined) { + value.rawContent = cbor.encode(new cbor.Tag(value.content, value.tag)); + } else { + value.rawContent = cbor.encode(value.content); + } + } + + value.rawContent.forEach(byte => output.writeByte(byte)); + } + + measureContent(value: CBORBox, measurer: bin.IMeasurer): bin.IMeasurer { + if (!value.rawContent) { + if (value.tag === undefined) { + value.rawContent = cbor.encode(value.content); + } else { + value.rawContent = cbor.encode(new cbor.Tag(value.content, value.tag)); + } + } + + return measurer.add(value.rawContent.length); + } +} + +export class CBORBox extends Box { + public static readonly typeCode = 'cbor'; + public static readonly schema = new CBORBoxSchema(); + // see https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml for assigned tag numbers + public tag?: number; + public content: unknown; + public rawContent: Uint8Array | undefined; + + constructor() { + super(CBORBox.typeCode, CBORBox.schema); } public toString(prefix?: string): string { diff --git a/src/jumbf/CodestreamBox.ts b/src/jumbf/CodestreamBox.ts index 2854ba3b..9de00d28 100644 --- a/src/jumbf/CodestreamBox.ts +++ b/src/jumbf/CodestreamBox.ts @@ -1,15 +1,40 @@ +import * as bin from 'typed-binary'; import { Box } from './Box'; +import { BoxSchema } from './BoxSchema'; + +class CodestreamBoxSchema extends BoxSchema { + readContent(input: bin.ISerialInput, type: string, length: number): CodestreamBox { + if (type != CodestreamBox.typeCode) throw new Error(`CodestreamBox: Unexpected type ${type}`); + + const data = []; + for (let i = 0; i < length - 8; i++) { + data.push(input.readByte()); + } + + const box = new CodestreamBox(); + box.content = new Uint8Array(data); + + return box; + } + + writeContent(output: bin.ISerialOutput, value: CodestreamBox): void { + if (value.content) { + value.content.forEach(byte => output.writeByte(byte)); + } + } + + measureContent(value: CodestreamBox, measurer: bin.IMeasurer): bin.IMeasurer { + return measurer.add(value.content ? value.content.length : 0); + } +} export class CodestreamBox extends Box { public static readonly typeCode = 'jp2c'; + public static readonly schema = new CodestreamBoxSchema(); public content?: Uint8Array; constructor() { - super(CodestreamBox.typeCode); - } - - public parse(buf: Uint8Array) { - this.content = buf; + super(CodestreamBox.typeCode, CodestreamBox.schema); } public toString(prefix?: string | undefined): string { diff --git a/src/jumbf/DescriptionBox.ts b/src/jumbf/DescriptionBox.ts index db9aa17c..a6430f53 100644 --- a/src/jumbf/DescriptionBox.ts +++ b/src/jumbf/DescriptionBox.ts @@ -1,60 +1,93 @@ +import * as bin from 'typed-binary'; import { BinaryHelper } from '../util'; import { Box } from './Box'; -import { BoxReader } from './BoxReader'; +import { BoxSchema } from './BoxSchema'; +import { GenericBoxSchema } from './GenericBoxSchema'; import { IBox } from './IBox'; +import * as schemata from './schemata'; -export class DescriptionBox extends Box { - public static readonly typeCode = 'jumd'; - public uuid: Uint8Array = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - public requestable?: boolean; - public label: string | undefined; - public id: number | undefined; - public hash: Uint8Array | undefined; - public privateBoxes: IBox[] = []; - - constructor() { - super(DescriptionBox.typeCode); - } - - public parse(buf: Uint8Array) { - if (buf.length < 17) { - throw new Error('DescriptionBox: Data too short'); - } - - this.uuid = buf.subarray(0, 16); - const toggles = buf[16]; +class DescriptionBoxSchema extends BoxSchema { + readonly uuid = schemata.uuid; + readonly toggles = bin.byte; + readonly label = bin.string; + readonly id = bin.u32; + readonly hash = bin.arrayOf(bin.byte, 32); + // Note: This doesn't work due to a circular import. + // readonly privateBoxes = new GenericBoxSchema(); - this.requestable = (toggles & 1) === 1; - - buf = buf.subarray(17); + readContent(input: bin.ISerialInput, type: string, length: number): DescriptionBox { + if (type != DescriptionBox.typeCode) throw new Error(`DescriptionBox: Unexpected type ${type}`); + const end = input.currentByteOffset + length - 8; + const box = new DescriptionBox(); + box.uuid = this.uuid.read(input); + const toggles = this.toggles.read(input); + box.requestable = (toggles & 1) === 1; if ((toggles & 0b10) === 0b10) { - if (!buf.length) throw new Error('DescriptionBox: Label present but data too short'); - const { string, bytesRead } = BinaryHelper.readNullTerminatedString(buf, 0); - this.label = string; - buf = buf.subarray(bytesRead); + box.label = this.label.read(input); } - if ((toggles & 0b100) === 0b100) { - if (buf.length < 4) throw new Error('DescriptionBox: ID present but data too short'); - this.id = BinaryHelper.readUInt32(buf, 0); - buf = buf.subarray(4); + box.id = this.id.read(input); } - if ((toggles & 0b1000) == 0b1000) { - if (buf.length < 32) throw new Error('DescriptionBox: Signature present but data too short'); - this.hash = buf.subarray(0, 32); - buf = buf.subarray(32); + box.hash = new Uint8Array(this.hash.read(input)); } - if ((toggles & 0b10000) == 0b10000) { - if (!buf.length) throw new Error('DescriptionBox: Private field present but data too short'); - while (buf.length > 0) { - const { box, lBox } = BoxReader.readFromBuffer(buf); - this.privateBoxes.push(box); - buf = buf.subarray(lBox); + const nestedBoxSchema = new GenericBoxSchema(); + while (input.currentByteOffset < end) { + const nestedBox = nestedBoxSchema.read(input); + box.privateBoxes.push(nestedBox); } + if (input.currentByteOffset > end) + throw new Error( + `DescriptionBox: Private field data exceeded box length by ${input.currentByteOffset - end} bytes`, + ); } + + return box; + } + + writeContent(output: bin.ISerialOutput, value: DescriptionBox): void { + this.uuid.write(output, value.uuid); + const toggles = + (value.requestable ? 1 : 0) + + (value.label ? 0b10 : 0) + + (value.id ? 0b100 : 0) + + (value.hash ? 0b1000 : 0) + + (value.privateBoxes.length ? 0b10000 : 0); + this.toggles.write(output, toggles); + if (value.label) this.label.write(output, value.label); + if (value.id) this.id.write(output, value.id); + if (value.hash) this.hash.write(output, Array.from(value.hash)); + value.privateBoxes.forEach(box => { + box.schema.write(output, box); + }); + } + + measureContent(value: DescriptionBox, measurer: bin.IMeasurer): bin.IMeasurer { + return measurer.add( + this.uuid.measure(value.uuid).size + + 1 + // toggles + (value.label ? value.label.length + 1 : 0) + + (value.id ? 4 : 0) + + (value.hash ? 32 : 0) + + value.privateBoxes.reduce((acc, box) => acc + box.schema.measure(box).size, 0), + ); + } +} + +export class DescriptionBox extends Box { + public static readonly typeCode = 'jumd'; + public static readonly schema = new DescriptionBoxSchema(); + public uuid: Uint8Array = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + public requestable?: boolean; + public label: string | undefined; + public id: number | undefined; + public hash: Uint8Array | undefined; + public privateBoxes: IBox[] = []; + + constructor() { + super(DescriptionBox.typeCode, DescriptionBox.schema); } public toString(): string { diff --git a/src/jumbf/EmbeddedFileBox.ts b/src/jumbf/EmbeddedFileBox.ts index c185a72c..3fe60f5f 100644 --- a/src/jumbf/EmbeddedFileBox.ts +++ b/src/jumbf/EmbeddedFileBox.ts @@ -1,15 +1,44 @@ +import * as bin from 'typed-binary'; import { Box } from './Box'; +import { BoxSchema } from './BoxSchema'; +import * as schemata from './schemata'; + +class EmbeddedFileBoxSchema extends BoxSchema { + readonly length = schemata.length; + readonly type = schemata.type; + + readContent(input: bin.ISerialInput, type: string, length: number): EmbeddedFileBox { + if (type != EmbeddedFileBox.typeCode) throw new Error(`EmbeddedFileBox: Unexpected type ${type}`); + + const data = []; + for (let i = 0; i < length - 8; i++) { + data.push(input.readByte()); + } + + const box = new EmbeddedFileBox(); + box.content = new Uint8Array(data); + + return box; + } + + writeContent(output: bin.ISerialOutput, value: EmbeddedFileBox): void { + if (value.content) { + value.content.forEach(byte => output.writeByte(byte)); + } + } + + measureContent(value: EmbeddedFileBox, measurer: bin.IMeasurer): bin.IMeasurer { + return measurer.add(value.content ? value.content.length : 0); + } +} export class EmbeddedFileBox extends Box { public static readonly typeCode = 'bidb'; + public static readonly schema = new EmbeddedFileBoxSchema(); public content?: Uint8Array; constructor() { - super(EmbeddedFileBox.typeCode); - } - - public parse(buf: Uint8Array) { - this.content = buf; + super(EmbeddedFileBox.typeCode, EmbeddedFileBox.schema); } public toString(prefix?: string | undefined): string { diff --git a/src/jumbf/EmbeddedFileDescriptionBox.ts b/src/jumbf/EmbeddedFileDescriptionBox.ts index 76f7f083..07632ba4 100644 --- a/src/jumbf/EmbeddedFileDescriptionBox.ts +++ b/src/jumbf/EmbeddedFileDescriptionBox.ts @@ -1,36 +1,56 @@ -import { BinaryHelper } from '../util'; +import * as bin from 'typed-binary'; import { Box } from './Box'; +import { BoxSchema } from './BoxSchema'; -export class EmbeddedFileDescriptionBox extends Box { - public static readonly typeCode = 'bfdb'; - public mediaType?: string; - public fileName?: string; - - constructor() { - super(EmbeddedFileDescriptionBox.typeCode); - } +class EmbeddedFileDescriptionBoxSchema extends BoxSchema { + readonly flags = bin.byte; + readonly fileName = bin.string; + readonly mediaType = bin.string; - public parse(buf: Uint8Array) { - if (buf.length < 2) throw new Error('Embedded file description box too short'); + readContent( + input: bin.ISerialInput, + type: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + length: number, + ): EmbeddedFileDescriptionBox { + if (type != EmbeddedFileDescriptionBox.typeCode) + throw new Error(`EmbeddedFileDescriptionBox: Unexpected type ${type}`); - const hasFileName = buf[0] && 1 === 1; - if (hasFileName && buf.length < 3) throw new Error('Embedded file description box too short'); + const flags = this.flags.read(input); - buf = buf.subarray(1); + const box = new EmbeddedFileDescriptionBox(); - if (hasFileName) { - const { string: s, bytesRead } = BinaryHelper.readNullTerminatedString(buf, 0); - // If we have already reached to the end we are missing one null terminator - if (bytesRead === buf.length) throw new Error('Embedded file description box invalid'); - this.mediaType = s; - buf = buf.subarray(bytesRead); + box.mediaType = this.mediaType.read(input); + if (flags & 1) { + box.fileName = this.fileName.read(input); } - const { string: s, bytesRead } = BinaryHelper.readNullTerminatedString(buf, 0); - // We expect to read all the way to the end - if (bytesRead !== buf.length) throw new Error('Embedded file description box invalid'); - if (hasFileName) this.fileName = s; - else this.mediaType = s; + return box; + } + + writeContent(output: bin.ISerialOutput, value: EmbeddedFileDescriptionBox): void { + this.flags.write(output, value.fileName ? 1 : 0); + this.mediaType.write(output, value.mediaType ?? ''); + if (value.fileName) this.fileName.write(output, value.fileName); + } + + measureContent(value: EmbeddedFileDescriptionBox, measurer: bin.IMeasurer): bin.IMeasurer { + return measurer.add( + 1 + // flags + this.mediaType.measure(value.mediaType ?? '').size + + (value.fileName ? this.fileName.measure(value.fileName).size : 0), + ); + } +} + +export class EmbeddedFileDescriptionBox extends Box { + public static readonly typeCode = 'bfdb'; + public static readonly schema = new EmbeddedFileDescriptionBoxSchema(); + public mediaType?: string; + public fileName?: string; + + constructor() { + super(EmbeddedFileDescriptionBox.typeCode, EmbeddedFileDescriptionBox.schema); } public toString(prefix?: string | undefined): string { diff --git a/src/jumbf/GenericBoxSchema.ts b/src/jumbf/GenericBoxSchema.ts new file mode 100644 index 00000000..d198745a --- /dev/null +++ b/src/jumbf/GenericBoxSchema.ts @@ -0,0 +1,74 @@ +import * as bin from 'typed-binary'; +import { C2PASaltBox } from './C2PASaltBox'; +import { CBORBox } from './CBORBox'; +import { CodestreamBox } from './CodestreamBox'; +import { DescriptionBox } from './DescriptionBox'; +import { EmbeddedFileBox } from './EmbeddedFileBox'; +import { EmbeddedFileDescriptionBox } from './EmbeddedFileDescriptionBox'; +import { IBox } from './IBox'; +import { JSONBox } from './JSONBox'; +import * as schemata from './schemata'; +import { SuperBox } from './SuperBox'; +import { UUIDBox } from './UUIDBox'; + +// generic box schema +// +// This generic schema delegates to the appropriate specific schema. +// For that, it either looks at the input stream or simply uses the +// given box's schema. +export class GenericBoxSchema extends bin.Schema { + readonly length = schemata.length; + readonly type = schemata.type; + + read(input: bin.ISerialInput): IBox { + // Read the header (length and type) and then rewind to the + // previous position. This is a bit ugly, because we read some + // data twice, but unavoidable. Also, it allows us to handle + // unknown box types, even though we don't do that currently. + const length = this.length.read(input); + // There are special (low) values for length but we don't support them + if (length < 8) { + throw new Error(`JUMBFGenericBox: Invalid box length ${length}`); + } + const type = this.type.read(input); + input.skipBytes(-8); + + const schema = GenericBoxSchema.getSchema(type); + return schema.read(input); + } + + write(output: bin.ISerialOutput, value: IBox): void { + // delegate to the box's schema + return value.schema.write(output, value); + } + + measure(value: IBox, measurer: bin.IMeasurer = new bin.Measurer()): bin.IMeasurer { + // delegate to the box's schema + return value.schema.measure(value, measurer); + } + + private static getSchema(type: string): bin.Schema { + switch (type) { + case C2PASaltBox.typeCode: + return C2PASaltBox.schema; + case CBORBox.typeCode: + return CBORBox.schema; + case CodestreamBox.typeCode: + return CodestreamBox.schema; + case DescriptionBox.typeCode: + return DescriptionBox.schema; + case EmbeddedFileBox.typeCode: + return EmbeddedFileBox.schema; + case EmbeddedFileDescriptionBox.typeCode: + return EmbeddedFileDescriptionBox.schema; + case JSONBox.typeCode: + return JSONBox.schema; + case SuperBox.typeCode: + return SuperBox.schema; + case UUIDBox.typeCode: + return UUIDBox.schema; + default: + return schemata.fallback; + } + } +} diff --git a/src/jumbf/IBox.ts b/src/jumbf/IBox.ts index 95c8a2a0..d6f04922 100644 --- a/src/jumbf/IBox.ts +++ b/src/jumbf/IBox.ts @@ -1,5 +1,7 @@ +import * as bin from 'typed-binary'; + export interface IBox { type: string; - parse(buf: Uint8Array, urlPrefix?: string): void; + schema: bin.ISchema; toString(prefix?: string): string; } diff --git a/src/jumbf/JSONBox.ts b/src/jumbf/JSONBox.ts index 9249126f..9cef58de 100644 --- a/src/jumbf/JSONBox.ts +++ b/src/jumbf/JSONBox.ts @@ -1,21 +1,52 @@ -import { BinaryHelper } from '../util'; +import * as bin from 'typed-binary'; import { Box } from './Box'; +import { BoxSchema } from './BoxSchema'; +import * as schemata from './schemata'; -export class JSONBox extends Box { - public static readonly typeCode = 'json'; - public content: unknown; +// TODO: JSON is UTF-8, but we're reading bytes as if they were codepoints here +class JSONBoxSchema extends BoxSchema { + readonly length = schemata.length; + readonly type = schemata.type; - constructor() { - super(JSONBox.typeCode); - } + readContent(input: bin.ISerialInput, type: string, length: number): JSONBox { + if (type != JSONBox.typeCode) throw new Error(`JSONBox: Unexpected type ${type}`); - public parse(buf: Uint8Array) { + let json = ''; + for (let i = 0; i < length - 8; i++) { + json += String.fromCharCode(input.readByte()); + } + + const box = new JSONBox(); try { - this.content = JSON.parse(BinaryHelper.readString(buf, 0, buf.length)); + box.content = json == '' ? undefined : JSON.parse(json); } catch { // TODO This needs to be properly reported as a validation error throw new Error('JSONBox: Invalid JSON data'); } + + return box; + } + + writeContent(output: bin.ISerialOutput, value: JSONBox): void { + const json = value.content === undefined ? '' : JSON.stringify(value.content); + + for (let i = 0; i != json.length; i++) output.writeByte(json.charCodeAt(i)); + } + + measureContent(value: JSONBox, measurer: bin.IMeasurer): bin.IMeasurer { + const json = value.content === undefined ? '' : JSON.stringify(value.content); + + return measurer.add(json.length); + } +} + +export class JSONBox extends Box { + public static readonly typeCode = 'json'; + public static readonly schema = new JSONBoxSchema(); + public content: unknown; + + constructor() { + super(JSONBox.typeCode, JSONBox.schema); } public toString(prefix?: string): string { diff --git a/src/jumbf/SuperBox.ts b/src/jumbf/SuperBox.ts index 07a088c6..b84bff30 100644 --- a/src/jumbf/SuperBox.ts +++ b/src/jumbf/SuperBox.ts @@ -1,41 +1,96 @@ +import * as bin from 'typed-binary'; import { BinaryHelper } from '../util'; import { Box } from './Box'; -import { BoxReader } from './BoxReader'; +import { BoxSchema } from './BoxSchema'; import { DescriptionBox } from './DescriptionBox'; +import { GenericBoxSchema } from './GenericBoxSchema'; import { IBox } from './IBox'; +class SuperBoxSchema extends BoxSchema { + // Note: This doesn't work due to a circular import. + // readonly contentBoxes = new GenericBoxSchema(); + + readContent(input: bin.ISerialInput, type: string, length: number): SuperBox { + if (type != SuperBox.typeCode) throw new Error(`SuperBox: Unexpected type ${type}`); + + const box = new SuperBox(); + + // read raw content excluding (length, type) header + const rawContentSchema = bin.arrayOf(bin.byte, length - 8); + const buf = rawContentSchema.read(input); + box.rawContent = new Uint8Array(buf); + input.skipBytes(-(length - 8)); + + const end = input.currentByteOffset + length - 8; + const nestedBoxSchema = new GenericBoxSchema(); + while (input.currentByteOffset < end) { + const nestedBox = nestedBoxSchema.read(input); + if (nestedBox instanceof DescriptionBox) { + box.descriptionBox = nestedBox; + } else { + box.contentBoxes.push(nestedBox); + } + } + if (input.currentByteOffset > end) + throw new Error( + `SuperBox: Private field data exceeded box length by ${input.currentByteOffset - end} bytes`, + ); + + if (!box.descriptionBox) throw new Error('SuperBox: Missing description box'); + + return box; + } + + writeContent(output: bin.ISerialOutput, value: SuperBox): void { + if (!value.descriptionBox) throw new Error('SuperBox: Missing description box'); + + value.descriptionBox.schema.write(output, value.descriptionBox); + value.contentBoxes.forEach(box => { + box.schema.write(output, box); + }); + } + + measureContent(value: SuperBox, measurer: bin.IMeasurer): bin.IMeasurer { + if (!value.descriptionBox) throw new Error('SuperBox: Missing description box'); + + return measurer.add( + value.descriptionBox.schema.measure(value.descriptionBox).size + // description box + value.contentBoxes.reduce((acc, box) => acc + box.schema.measure(box).size, 0), + ); + } +} + export class SuperBox extends Box { public static readonly typeCode = 'jumb'; + public static readonly schema = new SuperBoxSchema(); public descriptionBox?: DescriptionBox; public contentBoxes: IBox[] = []; public rawContent: Uint8Array | undefined; public uri: string | undefined; constructor() { - super(SuperBox.typeCode); + super(SuperBox.typeCode, SuperBox.schema); } public static fromBuffer(buf: Uint8Array): SuperBox { - const box = BoxReader.readFromBuffer(buf, 'self#jumbf=').box; - if (!(box instanceof SuperBox)) throw new Error('Outer box is not a JUMBF super box'); + const reader = new bin.BufferReader(buf, { endianness: 'big' }); + const box = SuperBox.schema.read(reader); + + const rootURI = 'self#jumbf='; + + // set URI fields on this and nested boxes + SuperBox.applyURI(box, rootURI); + return box; } - public parse(buf: Uint8Array, uriPrefix?: string) { - this.rawContent = buf; - - while (buf.length > 0) { - const { box, lBox } = BoxReader.readFromBuffer(buf, this.uri); - if (box instanceof DescriptionBox) { - this.descriptionBox = box; - if (uriPrefix && this.descriptionBox.label) this.uri = uriPrefix + '/' + this.descriptionBox.label; - } else { - this.contentBoxes.push(box); - } - buf = buf.subarray(lBox); + private static applyURI(box: SuperBox, uri: string) { + if (box.descriptionBox!.label) { + box.uri = `${uri}/${box.descriptionBox!.label}`; } - - if (!this.descriptionBox) throw new Error('Super box is missing description box'); + box.contentBoxes.forEach(subBox => { + if (subBox instanceof SuperBox) SuperBox.applyURI(subBox, box.uri!); + }); } public toString(prefix?: string) { diff --git a/src/jumbf/UUIDBox.ts b/src/jumbf/UUIDBox.ts index 417646e1..0765fa18 100644 --- a/src/jumbf/UUIDBox.ts +++ b/src/jumbf/UUIDBox.ts @@ -1,20 +1,46 @@ +import * as bin from 'typed-binary'; import { BinaryHelper } from '../util'; import { Box } from './Box'; +import { BoxSchema } from './BoxSchema'; +import * as schemata from './schemata'; + +class UUIDBoxSchema extends BoxSchema { + readonly uuid = schemata.uuid; + + readContent(input: bin.ISerialInput, type: string, length: number): UUIDBox { + if (type != UUIDBox.typeCode) throw new Error(`UUIDBox: Unexpected type ${type}`); + + const uuid = this.uuid.read(input); + const content = []; + for (let i = 0; i != length - 4 - 4 - 16; i++) { + content.push(input.readByte()); + } + const box = new UUIDBox(); + + box.uuid = uuid; + box.content = new Uint8Array(content); + + return box; + } + + writeContent(output: bin.ISerialOutput, value: UUIDBox): void { + this.uuid.write(output, value.uuid); + value.content?.forEach(byte => output.writeByte(byte)); + } + + measureContent(value: UUIDBox, measurer: bin.IMeasurer): bin.IMeasurer { + return measurer.add(this.uuid.measure(value.uuid).size + (value.content ? value.content.length : 0)); + } +} export class UUIDBox extends Box { public static readonly typeCode = 'uuid'; + public static readonly schema = new UUIDBoxSchema(); public uuid: Uint8Array = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); public content?: Uint8Array; constructor() { - super(UUIDBox.typeCode); - } - - public parse(buf: Uint8Array) { - if (buf.length < 16) throw new Error('UUIDBox: Data too short'); - - this.uuid = buf.subarray(0, 16); - this.content = buf.subarray(16); + super(UUIDBox.typeCode, UUIDBox.schema); } public toString(prefix?: string | undefined): string { diff --git a/src/jumbf/schemata.ts b/src/jumbf/schemata.ts new file mode 100644 index 00000000..e1873d0b --- /dev/null +++ b/src/jumbf/schemata.ts @@ -0,0 +1,91 @@ +// collection of schemata for serialization of JUMBF boxes + +import * as bin from 'typed-binary'; +import { Box } from './Box'; + +// length field of a box +export const length = bin.u32; + +// type code schema +// +// The JUMBF type code is a 4-byte string representing the type of a box. +// It is used to identify the type of data being serialized. +class JUMBFTypeCodeSchema extends bin.Schema { + read(input: bin.ISerialInput): string { + return String.fromCharCode(input.readByte(), input.readByte(), input.readByte(), input.readByte()); + } + + write(output: bin.ISerialOutput, value: string): void { + if (value.length != 4) throw new Error('JUMBFTypeCode: Invalid length'); + [0, 1, 2, 3].forEach(i => { + output.writeByte(value.charCodeAt(i)); + }); + } + + measure(_: string, measurer: bin.IMeasurer = new bin.Measurer()): bin.IMeasurer { + // The size of the data serialized by this schema + // doesn't depend on the actual value. It's always 4 bytes. + return measurer.add(4); + } +} +export const type = new JUMBFTypeCodeSchema(); + +// type field for UUIDs +class JUMBFUUIDSchema extends bin.Schema { + read(input: bin.ISerialInput): Uint8Array { + const uuid = []; + for (let i = 0; i != 16; i++) { + uuid.push(input.readByte()); + } + return new Uint8Array(uuid); + } + + write(output: bin.ISerialOutput, value: Uint8Array): void { + if (value.length != 16) throw new Error('JUMBFUUID: Invalid length'); + value.forEach(byte => output.writeByte(byte)); + } + + measure(_: Uint8Array, measurer: bin.IMeasurer = new bin.Measurer()): bin.IMeasurer { + // The size of the data serialized by this schema + // doesn't depend on the actual value. It's always 16 bytes. + return measurer.add(16); + } +} +export const uuid = new JUMBFUUIDSchema(); + +// fallback schema +// +// This schema only reads length and type but skips over the actual data. +// TODO: Either implement this (i.e. by storing the data) or remove it +// and treat unknown types as error. +class FallbackBoxSchema extends bin.Schema { + readonly length = length; + readonly type = type; + + read(input: bin.ISerialInput): Box { + // read the length and type, but just skip over the remaining data + const length = this.length.read(input); + const type = this.type.read(input); + input.skipBytes(length - 8); + + return new Box(type, this); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + write(output: bin.ISerialOutput, value: Box): void { + // not implemented: + // - We could (since we know the length), read and store the + // data as raw bytes, even without knowing their structure. + // - However, while reading and ignoring unknown data is okay, + // writing it is more problematic. + // Since the possible use cases are unclear, this isn't + // implemented at the moment. + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + measure(value: Box, measurer: bin.IMeasurer = new bin.Measurer()): bin.IMeasurer { + throw new Error('Method not implemented.'); + } +} +export const fallback = new FallbackBoxSchema(); diff --git a/tests/jumbf/C2PASaltBox.test.ts b/tests/jumbf/C2PASaltBox.test.ts new file mode 100644 index 00000000..54b9a78b --- /dev/null +++ b/tests/jumbf/C2PASaltBox.test.ts @@ -0,0 +1,96 @@ +import assert from 'assert'; +import * as bin from 'typed-binary'; +import { C2PASaltBox } from '../../src/jumbf/C2PASaltBox'; +import { BinaryHelper } from '../../src/util'; + +describe('C2PASaltBox Tests', function () { + this.timeout(0); + + describe('16 bit salt', function () { + const saltString = '6332637300110010800000aa00389b71'; + const serializedString = '00000018633273686332637300110010800000aa00389b71'; + + it('serialization', async function () { + const box = new C2PASaltBox(); + box.salt = BinaryHelper.fromHexString(saltString); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = C2PASaltBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof C2PASaltBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.salt); + assert.equal(BinaryHelper.toHexString(box.salt), saltString); + }); + }); + + describe('32 bit salt', function () { + const saltString = '0800000aa00386332637300116332637300110010010800000aa00389b719b71'; + const serializedString = '00000028633273680800000aa00386332637300116332637300110010010800000aa00389b719b71'; + + it('serialization', async function () { + const box = new C2PASaltBox(); + box.salt = BinaryHelper.fromHexString(saltString); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = C2PASaltBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof C2PASaltBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.salt); + assert.equal(BinaryHelper.toHexString(box.salt), saltString); + }); + }); +}); diff --git a/tests/jumbf/CBORBox.test.ts b/tests/jumbf/CBORBox.test.ts new file mode 100644 index 00000000..6c6a7319 --- /dev/null +++ b/tests/jumbf/CBORBox.test.ts @@ -0,0 +1,144 @@ +import assert from 'assert'; +import * as bin from 'typed-binary'; +import { CBORBox } from '../../src/jumbf'; +import { BinaryHelper } from '../../src/util'; + +describe('CBORBox Tests', function () { + this.timeout(0); + + describe('Empty', function () { + const serializedString = '0000000963626f72f7'; + + it('serialization', async function () { + const box = new CBORBox(); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = CBORBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof CBORBox)) assert.fail('resulting box has wrong type'); + assert.equal(box.content, undefined); + assert.ok(box.rawContent); + assert.equal(box.rawContent.length, 1); + }); + }); + + describe('Simple Dict', function () { + // Note: cbor-js encodes the dict as 'a1616101' while cbor-x uses 'b90001616101' + const serializedString = '0000000e63626f72b90001616101'; + + it('serialization', async function () { + const box = new CBORBox(); + box.content = { a: 1 }; + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = CBORBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof CBORBox)) assert.fail('resulting box has wrong type'); + assert.equal(JSON.stringify(box.content), JSON.stringify({ a: 1 })); + assert.ok(box.rawContent); + assert.equal(box.rawContent.length, 6); + }); + }); + + describe('Tagged Value', function () { + const serializedString = '0000000f63626f72d8641a66a4e9f1'; + const tag = 100; + const content = 1722083825; + + it('serialization', async function () { + const box = new CBORBox(); + box.tag = tag; + box.content = content; + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = CBORBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof CBORBox)) assert.fail('resulting box has wrong type'); + assert.equal(box.tag, tag); + assert.equal(box.content, content); + assert.ok(box.rawContent); + assert.equal(box.rawContent.length, 7); + }); + }); +}); diff --git a/tests/jumbf/CodestreamBox.test.ts b/tests/jumbf/CodestreamBox.test.ts new file mode 100644 index 00000000..4bbfeb5f --- /dev/null +++ b/tests/jumbf/CodestreamBox.test.ts @@ -0,0 +1,94 @@ +import assert from 'assert'; +import * as bin from 'typed-binary'; +import { CodestreamBox } from '../../src/jumbf'; +import { BinaryHelper } from '../../src/util'; + +describe('CodestreamBox Tests', function () { + this.timeout(0); + + describe('Empty', function () { + const serializedString = '000000086a703263'; + + it('serialization', async function () { + const box = new CodestreamBox(); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = CodestreamBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof CodestreamBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.content); + assert.equal(box.content.length, 0); + }); + }); + + describe('Not Empty', function () { + const contentString = '6332637300110010800000aa00389b71'; + const serializedString = '000000186a7032636332637300110010800000aa00389b71'; + + it('serialization', async function () { + const box = new CodestreamBox(); + box.content = BinaryHelper.fromHexString(contentString); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = CodestreamBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof CodestreamBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.content); + assert.equal(BinaryHelper.toHexString(box.content), contentString); + }); + }); +}); diff --git a/tests/jumbf/DescriptionBox.test.ts b/tests/jumbf/DescriptionBox.test.ts new file mode 100644 index 00000000..6aeded30 --- /dev/null +++ b/tests/jumbf/DescriptionBox.test.ts @@ -0,0 +1,174 @@ +import assert from 'assert'; +import * as bin from 'typed-binary'; +import { DescriptionBox } from '../../src/jumbf'; +import { UUIDBox } from '../../src/jumbf/UUIDBox'; +import { BinaryHelper } from '../../src/util'; + +describe('DescriptionBox Tests', function () { + this.timeout(0); + + describe('Minimal', function () { + const uuidString = '6332637300110010800000aa00389b71'; + const serializedString = '000000196a756d646332637300110010800000aa00389b7100'; + + it('serialization', async function () { + const box = new DescriptionBox(); + box.uuid = BinaryHelper.fromHexString(uuidString); + box.requestable = false; + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = DescriptionBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof DescriptionBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.uuid); + assert.equal(BinaryHelper.toHexString(box.uuid), uuidString); + assert.equal(box.requestable, false); + assert.equal(box.label, undefined); + assert.equal(box.id, undefined); + assert.equal(box.hash, undefined); + assert.equal(box.privateBoxes.length, 0); + }); + }); + + describe('With Optional Fields', function () { + const uuidString = '6332637300110010800000aa00389b71'; + const label = 'description label'; + const id = 42; + const hashString = '8dc6ba27eb4c0195fc7001c3e13ecaa78dc6ba27eb4c0195fc7001c3e13ecaa7'; + const serializedString = + '0000004f6a756d646332637300110010800000aa00389b710f6465736372697074696f6e206c6162656c000000002a8dc6ba27eb4c0195fc7001c3e13ecaa78dc6ba27eb4c0195fc7001c3e13ecaa7'; + + it('serialization', async function () { + const box = new DescriptionBox(); + box.uuid = BinaryHelper.fromHexString(uuidString); + box.requestable = true; + box.label = label; + box.id = id; + box.hash = BinaryHelper.fromHexString(hashString); + box.privateBoxes = []; + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = DescriptionBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof DescriptionBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.uuid); + assert.equal(BinaryHelper.toHexString(box.uuid), uuidString); + assert.equal(box.requestable, true); + assert.equal(box.label, label); + assert.equal(box.id, id); + assert.ok(box.hash); + assert.equal(BinaryHelper.toHexString(box.hash), hashString); + assert.equal(box.privateBoxes.length, 0); + }); + }); + + describe('With Private Boxes', function () { + const uuidString = '6332637300110010800000aa00389b71'; + const serializedString = + '000000316a756d646332637300110010800000aa00389b711000000018757569646332637300110010800000aa00389b71'; + + it('serialization', async function () { + const box = new DescriptionBox(); + box.uuid = BinaryHelper.fromHexString(uuidString); + box.requestable = false; + + const nestedBox = new UUIDBox(); + nestedBox.uuid = BinaryHelper.fromHexString(uuidString); + box.privateBoxes.push(nestedBox); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = DescriptionBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof DescriptionBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.uuid); + assert.equal(BinaryHelper.toHexString(box.uuid), uuidString); + assert.equal(box.requestable, false); + assert.equal(box.label, undefined); + assert.equal(box.id, undefined); + assert.equal(box.hash, undefined); + assert.equal(box.privateBoxes.length, 1); + assert(box.privateBoxes[0] instanceof UUIDBox); + }); + }); +}); diff --git a/tests/jumbf/EmbeddedFileBox.test.ts b/tests/jumbf/EmbeddedFileBox.test.ts new file mode 100644 index 00000000..8a477a03 --- /dev/null +++ b/tests/jumbf/EmbeddedFileBox.test.ts @@ -0,0 +1,94 @@ +import assert from 'assert'; +import * as bin from 'typed-binary'; +import { EmbeddedFileBox } from '../../src/jumbf'; +import { BinaryHelper } from '../../src/util'; + +describe('EmbeddedFileBox Tests', function () { + this.timeout(0); + + describe('Empty', function () { + const serializedString = '0000000862696462'; + + it('serialization', async function () { + const box = new EmbeddedFileBox(); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = EmbeddedFileBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof EmbeddedFileBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.content); + assert.equal(box.content.length, 0); + }); + }); + + describe('Not Empty', function () { + const contentString = '6332637300110010800000aa00389b71'; + const serializedString = '00000018626964626332637300110010800000aa00389b71'; + + it('serialization', async function () { + const box = new EmbeddedFileBox(); + box.content = BinaryHelper.fromHexString(contentString); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = EmbeddedFileBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof EmbeddedFileBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.content); + assert.equal(BinaryHelper.toHexString(box.content), contentString); + }); + }); +}); diff --git a/tests/jumbf/EmbeddedFileDescriptionBox.test.ts b/tests/jumbf/EmbeddedFileDescriptionBox.test.ts new file mode 100644 index 00000000..cf7fe91e --- /dev/null +++ b/tests/jumbf/EmbeddedFileDescriptionBox.test.ts @@ -0,0 +1,100 @@ +import assert from 'assert'; +import * as bin from 'typed-binary'; +import { EmbeddedFileDescriptionBox } from '../../src/jumbf'; +import { BinaryHelper } from '../../src/util'; + +describe('EmbeddedFileDescriptionBox Tests', function () { + this.timeout(0); + + describe('Without Filename', function () { + const mediaType = 'video/mp4'; + const serializedString = '000000136266646200766964656f2f6d703400'; + + it('serialization', async function () { + const box = new EmbeddedFileDescriptionBox(); + box.mediaType = mediaType; + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = EmbeddedFileDescriptionBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof EmbeddedFileDescriptionBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.mediaType); + assert.equal(box.mediaType, mediaType); + }); + }); + + describe('With Filename', function () { + const mediaType = 'video/mp4'; + const fileName = 'holiday.mp4'; + const serializedString = '0000001f6266646201766964656f2f6d703400686f6c696461792e6d703400'; + + it('serialization', async function () { + const box = new EmbeddedFileDescriptionBox(); + box.mediaType = mediaType; + box.fileName = fileName; + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = EmbeddedFileDescriptionBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof EmbeddedFileDescriptionBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.mediaType); + assert.equal(box.mediaType, mediaType); + assert.ok(box.fileName); + assert.equal(box.fileName, fileName); + }); + }); +}); diff --git a/tests/jumbf/GenericBoxSchema.test.ts b/tests/jumbf/GenericBoxSchema.test.ts new file mode 100644 index 00000000..242d142c --- /dev/null +++ b/tests/jumbf/GenericBoxSchema.test.ts @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import * as bin from 'typed-binary'; +import { GenericBoxSchema } from '../../src/jumbf/GenericBoxSchema'; +import { UUIDBox } from '../../src/jumbf/UUIDBox'; +import { BinaryHelper } from '../../src/util'; + +describe('GenericBoxSchema Tests', function () { + this.timeout(0); + + const schema = new GenericBoxSchema(); + + it('read an unrecognized box', async function () { + const serializedString = '000000107465787454727573744e5854'; + const buffer = BinaryHelper.fromHexString(serializedString); + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + assert.equal(box.type, 'text', 'type field was not filled'); + }); + + it('read a UUIDBox', async function () { + const serializedString = '00000018757569646332637300110010800000aa00389b71'; + + const buffer = BinaryHelper.fromHexString(serializedString); + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + assert(box instanceof UUIDBox); + }); +}); diff --git a/tests/jumbf/JSONBox.test.ts b/tests/jumbf/JSONBox.test.ts new file mode 100644 index 00000000..e34873f8 --- /dev/null +++ b/tests/jumbf/JSONBox.test.ts @@ -0,0 +1,91 @@ +import assert from 'assert'; +import * as bin from 'typed-binary'; +import { JSONBox } from '../../src/jumbf'; +import { BinaryHelper } from '../../src/util'; + +describe('JSONBox Tests', function () { + this.timeout(0); + + describe('Empty', function () { + const serializedString = '000000086a736f6e'; + + it('serialization', async function () { + const box = new JSONBox(); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = JSONBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof JSONBox)) assert.fail('resulting box has wrong type'); + assert.equal(box.content, undefined); + }); + }); + + describe('Simple Dict', function () { + const serializedString = '0000000f6a736f6e7b2261223a317d'; + + it('serialization', async function () { + const box = new JSONBox(); + box.content = { a: 1 }; + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = JSONBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof JSONBox)) assert.fail('resulting box has wrong type'); + assert.equal(JSON.stringify(box.content), JSON.stringify({ a: 1 })); + }); + }); +}); diff --git a/tests/jumbf/SuperBox.test.ts b/tests/jumbf/SuperBox.test.ts new file mode 100644 index 00000000..c69d327d --- /dev/null +++ b/tests/jumbf/SuperBox.test.ts @@ -0,0 +1,171 @@ +import assert from 'node:assert/strict'; +import * as bin from 'typed-binary'; +import { DescriptionBox, JSONBox, SuperBox } from '../../src/jumbf'; +import { BinaryHelper } from '../../src/util'; + +describe('SuperBox Tests', function () { + this.timeout(0); + + describe('Empty', function () { + const uuidString = '6332637300110010800000aa00389b71'; + const descriptionLabel = 'test.superbox'; + const serializedString = + '0000002f6a756d62000000276a756d646332637300110010800000aa00389b7102746573742e7375706572626f7800'; + const uri = 'self#jumbf=/test.superbox'; + + it('serialization', async function () { + const box = new SuperBox(); + box.descriptionBox = new DescriptionBox(); + box.descriptionBox.uuid = BinaryHelper.fromHexString(uuidString); + box.descriptionBox.label = descriptionLabel; + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = SuperBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof SuperBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.descriptionBox); + assert.ok(box.descriptionBox.uuid); + assert.equal(BinaryHelper.toHexString(box.descriptionBox.uuid), uuidString); + assert.ok(box.descriptionBox.label); + assert.equal(box.descriptionBox.label, descriptionLabel); + assert.ok(box.rawContent); + assert.equal(BinaryHelper.toHexString(box.rawContent), serializedString.slice(8 * 2)); + assert.equal(box.contentBoxes.length, 0); + }); + + it('deserialization from buffer', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // read the box from the buffer + const box = SuperBox.fromBuffer(buffer); + + // validate resulting box + if (!(box instanceof SuperBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.descriptionBox); + assert.ok(box.descriptionBox.uuid); + assert.equal(BinaryHelper.toHexString(box.descriptionBox.uuid), uuidString); + assert.ok(box.descriptionBox.label); + assert.equal(box.descriptionBox.label, descriptionLabel); + assert.ok(box.rawContent); + assert.equal(BinaryHelper.toHexString(box.rawContent), serializedString.slice(8 * 2)); + assert.equal(box.contentBoxes.length, 0); + assert.equal(box.uri, uri); + }); + }); + + describe('With nested JSON box', function () { + const uuidString = '6a736f6e00110010800000aa00389b71'; + const descriptionLabel = 'test.superbox'; + const nestedData = { key: 'value' }; + const serializedString = + '000000466a756d62000000276a756d646a736f6e00110010800000aa00389b7102746573742e7375706572626f7800000000176a736f6e7b226b6579223a2276616c7565227d'; + const uri = 'self#jumbf=/test.superbox'; + + it('serialization', async function () { + const box = new SuperBox(); + box.descriptionBox = new DescriptionBox(); + box.descriptionBox.uuid = BinaryHelper.fromHexString(uuidString); + box.descriptionBox.label = descriptionLabel; + + const nestedBox = new JSONBox(); + nestedBox.content = nestedData; + box.contentBoxes.push(nestedBox); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = SuperBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof SuperBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.descriptionBox); + assert.ok(box.descriptionBox.uuid); + assert.equal(BinaryHelper.toHexString(box.descriptionBox.uuid), uuidString); + assert.ok(box.descriptionBox.label); + assert.equal(box.descriptionBox.label, descriptionLabel); + assert.ok(box.rawContent); + assert.equal(BinaryHelper.toHexString(box.rawContent), serializedString.slice(8 * 2)); + + // validate nested box + assert.equal(box.contentBoxes.length, 1); + const nestedBox = box.contentBoxes[0]; + if (!(nestedBox instanceof JSONBox)) assert.fail('resulting nested box has wrong type'); + assert.equal(JSON.stringify(nestedBox.content), JSON.stringify(nestedData)); + }); + + it('deserialization from buffer', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // read the box from the buffer + const box = SuperBox.fromBuffer(buffer); + + // validate resulting box + if (!(box instanceof SuperBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.descriptionBox); + assert.ok(box.descriptionBox.uuid); + assert.equal(BinaryHelper.toHexString(box.descriptionBox.uuid), uuidString); + assert.ok(box.descriptionBox.label); + assert.equal(box.descriptionBox.label, descriptionLabel); + assert.ok(box.rawContent); + assert.equal(BinaryHelper.toHexString(box.rawContent), serializedString.slice(8 * 2)); + + // validate nested box + assert.equal(box.contentBoxes.length, 1); + const nestedBox = box.contentBoxes[0]; + if (!(nestedBox instanceof JSONBox)) assert.fail('resulting nested box has wrong type'); + assert.equal(JSON.stringify(nestedBox.content), JSON.stringify(nestedData)); + assert.equal(box.uri, uri); + }); + }); +}); diff --git a/tests/jumbf/UUIDBox.test.ts b/tests/jumbf/UUIDBox.test.ts new file mode 100644 index 00000000..bbd1c77b --- /dev/null +++ b/tests/jumbf/UUIDBox.test.ts @@ -0,0 +1,127 @@ +import assert from 'assert'; +import * as bin from 'typed-binary'; +import { UUIDBox } from '../../src/jumbf/UUIDBox'; +import { BinaryHelper } from '../../src/util'; + +describe('UUIDBox Tests', function () { + this.timeout(0); + + describe('Minimal', function () { + const uuidString = '6332637300110010800000aa00389b71'; + const serializedString = '00000018757569646332637300110010800000aa00389b71'; + + it('serialization', async function () { + const box = new UUIDBox(); + box.uuid = BinaryHelper.fromHexString(uuidString); + box.content = new Uint8Array(); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + // Special case for empty content + // This wouldn't be needed if `content` wasn't allowed to be + // `undefined` which should perhaps be changed (TODO). + it('serialization with undefined content', async function () { + const box = new UUIDBox(); + box.uuid = BinaryHelper.fromHexString(uuidString); + box.content = undefined; + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = UUIDBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof UUIDBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.uuid); + assert.equal(BinaryHelper.toHexString(box.uuid), uuidString); + assert.ok(box.content); + assert.equal(BinaryHelper.toHexString(box.content), ''); + }); + }); + + describe('With Content', function () { + const uuidString = '6332637300110010800000aa00389b71'; + const contentString = '7465737420646174610a'; + const serializedString = '00000022757569646332637300110010800000aa00389b717465737420646174610a'; + + it('serialization', async function () { + const box = new UUIDBox(); + box.uuid = BinaryHelper.fromHexString(uuidString); + box.content = BinaryHelper.fromHexString(contentString); + + // fetch schema from the box + const schema = box.schema; + + // write the box to a buffer + const length = schema.measure(box).size; + const buffer = Buffer.alloc(length); + const writer = new bin.BufferWriter(buffer, { endianness: 'big' }); + schema.write(writer, box); + + // verify that the expected buffer size was also used + assert.equal(buffer.length, writer.currentByteOffset, 'produced number of bytes differs'); + + // verify expected buffer contents + assert.equal(BinaryHelper.toHexString(buffer), serializedString); + }); + + it('deserialization', async function () { + const buffer = BinaryHelper.fromHexString(serializedString); + + // fetch schema from the box class + const schema = UUIDBox.schema; + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + // validate resulting box + if (!(box instanceof UUIDBox)) assert.fail('resulting box has wrong type'); + assert.ok(box.uuid); + assert.equal(BinaryHelper.toHexString(box.uuid), uuidString); + assert.ok(box.content); + assert.equal(BinaryHelper.toHexString(box.content), contentString); + }); + }); +}); diff --git a/tests/jumbf/schemata.test.ts b/tests/jumbf/schemata.test.ts new file mode 100644 index 00000000..cbfa48d6 --- /dev/null +++ b/tests/jumbf/schemata.test.ts @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import * as bin from 'typed-binary'; +import * as schemata from '../../src/jumbf/schemata'; +import { BinaryHelper } from '../../src/util'; + +describe('Schemata Tests', function () { + this.timeout(0); + + describe('FallbackBoxSchema Tests', function () { + const schema = schemata.fallback; + + it('read an unrecognized box', async function () { + const serializedString = '000000107465787454727573744e5854'; + const buffer = BinaryHelper.fromHexString(serializedString); + + // read the box from the buffer + const reader = new bin.BufferReader(buffer, { endianness: 'big' }); + const box = schema.read(reader); + + // verify that the expected buffer size was also used + assert.equal(reader.currentByteOffset, buffer.length, 'consumed number of bytes differs'); + + assert.equal(reader.currentByteOffset, buffer.length, 'not all data was consumed'); + assert.equal(box.type, 'text', 'type field was not filled'); + }); + }); +});