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

Expose read/write lengthcode for CDR2 emheaders #20

Merged
merged 4 commits into from
Nov 16, 2023
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
32 changes: 31 additions & 1 deletion src/CdrReader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,19 +277,49 @@ Object {
[true, 63, 9],
[false, 127, 0xffffffff],
])(
"round trips EMHEADER values with mustUnderstand: %d, id: %d, and size: %d",
"round trips EMHEADER values with mustUnderstand: %d, id: %d, and size: %d without lengthCode",
(mustUnderstand: boolean, id: number, objectSize: number) => {
const writer = new CdrWriter({ kind: EncapsulationKind.PL_CDR2_LE });

writer.emHeader(mustUnderstand, id, objectSize);

const reader = new CdrReader(writer.data);
// don't want to test default assignment of length code here
const { lengthCode, ...headerNoLengthCode } = reader.emHeader();

expect(headerNoLengthCode).toEqual({
objectSize,
id,
mustUnderstand,
});
// should be defined because of CDR2
expect(lengthCode).toBeDefined();
},
);
it.each([
[true, 100, 1, 0], // LC 0, 1 byte
[false, 200, 2, 1], // LC 1, 2 bytes
[false, 1028, 4, 2], // LC 2, 4 bytes
[false, 65, 8, 3], // LC 3, 8 bytes
[true, 63, 9, 4], // LC 4, any size
[false, 127, 0xffffffff, 5], // LC 5, any size
[false, 65, 12, 6], // LC 6, multiple of 4 bytes
[false, 65, 32, 7], // LC 7, multiple of 8 bytes
Comment on lines +300 to +307
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😵‍💫

])(
"round trips EMHEADER values with mustUnderstand: %d, id: %d, size: %d, and lengthCode: %d",
(mustUnderstand: boolean, id: number, objectSize: number, lengthCode: number) => {
const writer = new CdrWriter({ kind: EncapsulationKind.PL_CDR2_LE });

writer.emHeader(mustUnderstand, id, objectSize, lengthCode);

const reader = new CdrReader(writer.data);
const header = reader.emHeader();

expect(header).toEqual({
objectSize,
id,
mustUnderstand,
lengthCode,
});
},
);
Expand Down
32 changes: 21 additions & 11 deletions src/CdrReader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EncapsulationKind } from "./EncapsulationKind";
import { getEncapsulationKindInfo } from "./getEncapsulationKindInfo";
import { isBigEndian } from "./isBigEndian";
import { LengthCode, lengthCodeToObjectSizes } from "./lengthCodes";
import { EXTENDED_PID, SENTINEL_PID } from "./reservedPIDs";

interface Indexable {
Expand Down Expand Up @@ -185,9 +186,11 @@ export class CdrReader {
}

/**
* Reads the member header (EMHEADER) and returns the member ID, mustUnderstand flag, and object size
* Reads the member header (EMHEADER) and returns the member ID, mustUnderstand flag, and object size with optional length code
* The length code is only present in CDR2 and should prompt objectSize to be used in place of sequence length if applicable.
* See Extensible and Dynamic Topic Types (DDS-XTypes) v1.3 @ `7.4.3.4.2` for more info about CDR2 EMHEADER composition.
*/
emHeader(): { mustUnderstand: boolean; id: number; objectSize: number } {
emHeader(): { mustUnderstand: boolean; id: number; objectSize: number; lengthCode?: LengthCode } {
if (this.isCDR2) {
return this.memberHeaderV2();
} else {
Expand All @@ -196,7 +199,11 @@ export class CdrReader {
}

/** XCDR1 PL_CDR encapsulation parameter header*/
private memberHeaderV1(): { id: number; objectSize: number; mustUnderstand: boolean } {
private memberHeaderV1(): {
id: number;
objectSize: number;
mustUnderstand: boolean;
} {
// 4-byte header with two 16-bit fields
this.align(4);
const idHeader = this.uint16();
Expand Down Expand Up @@ -260,35 +267,37 @@ export class CdrReader {
}
}

private memberHeaderV2(): { id: number; objectSize: number; mustUnderstand: boolean } {
private memberHeaderV2(): {
id: number;
objectSize: number;
mustUnderstand: boolean;
lengthCode: LengthCode;
} {
const header = this.uint32();
// EMHEADER = (M_FLAG<<31) + (LC<<28) + M.id
// M is the member of a structure
// M_FLAG is the value of the Must Understand option for the member
const mustUnderstand = Math.abs((header & 0x80000000) >> 31) === 1;
// LC is the value of the Length Code for the member.
const lengthCode = (header & 0x70000000) >> 28;
const lengthCode = ((header & 0x70000000) >> 28) as LengthCode;
const id = header & 0x0fffffff;

const objectSize = this.emHeaderObjectSize(lengthCode);

return { mustUnderstand, id, objectSize };
return { mustUnderstand, id, objectSize, lengthCode };
}

/** Uses the length code to derive the member object size in
* the EMHEADER, sometimes reading NEXTINT (the next uint32
* following the header) from the buffer */
private emHeaderObjectSize(lengthCode: number) {
private emHeaderObjectSize(lengthCode: LengthCode) {
// 7.4.3.4.2 Member Header (EMHEADER), Length Code (LC) and NEXTINT
switch (lengthCode) {
case 0:
return 1;
case 1:
return 2;
case 2:
return 4;
case 3:
return 8;
return lengthCodeToObjectSizes[lengthCode];
// LC > 3 -> NEXTINT exists after header
case 4:
case 5:
Expand All @@ -300,6 +309,7 @@ export class CdrReader {
return 8 * this.uint32();
default:
throw new Error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Invalid length code ${lengthCode} in EMHEADER at offset ${this.offset - 4}`,
);
}
Expand Down
96 changes: 67 additions & 29 deletions src/CdrWriter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EncapsulationKind } from "./EncapsulationKind";
import { getEncapsulationKindInfo } from "./getEncapsulationKindInfo";
import { isBigEndian } from "./isBigEndian";
import { LengthCode, getLengthCodeForObjectSize, lengthCodeToObjectSizes } from "./lengthCodes";
import { EXTENDED_PID, SENTINEL_PID } from "./reservedPIDs";

export type CdrWriterOpts = {
Expand Down Expand Up @@ -184,12 +185,32 @@ export class CdrWriter {
}

/**
* Writes the member header (EMHEADER): mustUnderstand flag, the member ID, and object size
* Writes the member header (EMHEADER)
* Accomodates for PL_CDR and PL_CDR2 based on the CdrWriter constructor options
*
* @param mustUnderstand - Whether the member is required to be understood by the receiver
* @param id - The member ID
* @param objectSize - The size of the member in bytes
* @param lengthCode - Optional length code for CDR2 emHeaders.
* lengthCode values [5-7] allow the emHeader object size to take the place of the normally encoded member length.
*
* NOTE: Dynamically determines default value if not provided that does not affect serialization ie will use lengthCode values [0-4].
*
* From Extensible and Dynamic Topic Types in DDS-XTypes v1.3 @ `7.4.3.4.2`:
* "EMHEADER1 with LC values 5 to 7 also affect the serialization/deserialization virtual machine in that they cause NEXTINT to be
* reused also as part of the serialized member. This is useful because the serialization of certain members also starts with an
* integer length, which would take exactly the same value as NEXTINT. Therefore the use of length codes 5 to 7 saves 4 bytes in
* the serialization."
* @returns - CdrWriter instance
*/
emHeader(mustUnderstand: boolean, id: number, objectSize: number): CdrWriter {
emHeader(
mustUnderstand: boolean,
id: number,
objectSize: number,
lengthCode?: number,
): CdrWriter {
return this.isCDR2
? this.memberHeaderV2(mustUnderstand, id, objectSize)
? this.memberHeaderV2(mustUnderstand, id, objectSize, lengthCode as LengthCode)
: this.memberHeaderV1(mustUnderstand, id, objectSize);
}

Expand Down Expand Up @@ -231,7 +252,12 @@ export class CdrWriter {
return this;
}

private memberHeaderV2(mustUnderstand: boolean, id: number, objectSize: number): CdrWriter {
private memberHeaderV2(
mustUnderstand: boolean,
id: number,
objectSize: number,
lengthCode?: LengthCode,
): CdrWriter {
if (id > 0x0fffffff) {
// first byte is used for M_FLAG and LC
throw Error(`Member ID ${id} is too large. Max value is ${0x0fffffff}`);
Expand All @@ -241,37 +267,49 @@ export class CdrWriter {
// M_FLAG is the value of the Must Understand option for the member
const mustUnderstandFlag = mustUnderstand ? 1 << 31 : 0;
// LC is the value of the Length Code for the member.
let lengthCode: number | undefined;
switch (objectSize) {
const finalLengthCode: LengthCode = lengthCode ?? getLengthCodeForObjectSize(objectSize);

const header = mustUnderstandFlag | (finalLengthCode << 28) | id;

this.uint32(header);

switch (finalLengthCode) {
case 0:
case 1:
lengthCode = 0;
break;
case 2:
lengthCode = 1;
case 3: {
const shouldBeSize = lengthCodeToObjectSizes[finalLengthCode];
if (objectSize !== shouldBeSize) {
throw new Error(
`Cannot write a length code ${finalLengthCode} header with an object size not equal to ${shouldBeSize}`,
);
}
break;
}
// When the length code is > 3 the header is 8 bytes because of the NEXTINT value storing the object size
case 4:
lengthCode = 2;
case 5:
this.uint32(objectSize);
break;
case 8:
lengthCode = 3;
case 6:
if (objectSize % 4 !== 0) {
throw new Error(
"Cannot write a length code 6 header with an object size that is not a multiple of 4",
);
}
this.uint32(objectSize >> 2);
break;
}

if (lengthCode == undefined) {
// Not currently supporting writing of lengthCodes > 4
if (objectSize > 0xffffffff) {
throw Error(`Object size ${objectSize} for EMHEADER too large. Max size is ${0xfffffffff}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we ever hit this error in the past? Or if not does that mean we are just adding support for this in the writer so that we can use it in our unit tests (we won't actually be using lengthCodes > 4 in production anywhere)?

Copy link
Contributor Author

@snosenzo snosenzo Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really only use this for unit tests, so we haven't ever hit it before. I don't know about downstream users of this writer though. I'm adding this capability to the writer only for unit tests.

}
lengthCode = 4;
}

const header = mustUnderstandFlag | (lengthCode << 28) | id;

this.uint32(header);

// When the length code is > 3 the header is 8 bytes because of the NEXTINT value storing the object size
if (lengthCode >= 4) {
this.uint32(objectSize);
case 7:
if (objectSize % 8 !== 0) {
throw new Error(
"Cannot write a length code 7 header with an object size that is not a multiple of 8",
);
}
this.uint32(objectSize >> 3);
break;
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unexpected length code ${finalLengthCode}`);
}

return this;
Expand Down
38 changes: 38 additions & 0 deletions src/lengthCodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export type LengthCode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;

export function getLengthCodeForObjectSize(objectSize: number): LengthCode {
let defaultLengthCode: LengthCode | undefined;

switch (objectSize) {
case 1:
defaultLengthCode = 0;
break;
case 2:
defaultLengthCode = 1;
break;
case 4:
defaultLengthCode = 2;
break;
case 8:
defaultLengthCode = 3;
break;
}

if (defaultLengthCode == undefined) {
// Not currently supporting writing of lengthCodes > 4
if (objectSize > 0xffffffff) {
throw Error(
`Object size ${objectSize} for EMHEADER too large without specifying length code. Max size is ${0xffffffff}`,
);
}
defaultLengthCode = 4;
}
return defaultLengthCode;
}

export const lengthCodeToObjectSizes = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could :just: do 1 << objectSize

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but I like having the const type here to reflect the pairing in the spec

0: 1,
1: 2,
2: 4,
3: 8,
};
Loading