Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full serialization using iwoplaza/typed-binary package #25

Merged
merged 19 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
8 changes: 4 additions & 4 deletions src/jumbf/Box.ts
Original file line number Diff line number Diff line change
@@ -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<Box>;

constructor(type: string) {
constructor(type: string, schema: bin.ISchema<Box>) {
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}`;
}
Expand Down
62 changes: 7 additions & 55 deletions src/jumbf/BoxReader.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
}
44 changes: 44 additions & 0 deletions src/jumbf/BoxSchema.ts
Original file line number Diff line number Diff line change
@@ -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<T>`. The only difference
* is when reading, wher the length and type are supplied as
* additional parameters.
*/
export abstract class BoxSchema<TBox extends IBox> extends bin.Schema<TBox> {
readonly length = schemata.length;
readonly type = schemata.type;

public read(input: bin.ISerialInput): bin.Parsed<TBox> {
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<TBox>): 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<TBox> | 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<TBox>;
abstract writeContent(output: bin.ISerialOutput, value: bin.Parsed<TBox>): void;
abstract measureContent(value: bin.Parsed<TBox> | bin.MaxValue, measurer: bin.IMeasurer): bin.IMeasurer;
}
35 changes: 29 additions & 6 deletions src/jumbf/C2PASaltBox.ts
Original file line number Diff line number Diff line change
@@ -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<C2PASaltBox> {
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 {
Expand Down
82 changes: 68 additions & 14 deletions src/jumbf/CBORBox.ts
Original file line number Diff line number Diff line change
@@ -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<CBORBox> {
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 {
Expand Down
35 changes: 30 additions & 5 deletions src/jumbf/CodestreamBox.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
import * as bin from 'typed-binary';
import { Box } from './Box';
import { BoxSchema } from './BoxSchema';

class CodestreamBoxSchema extends BoxSchema<CodestreamBox> {
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 {
Expand Down
Loading