Skip to content
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
6 changes: 6 additions & 0 deletions examples/commons/operators.cddl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ label = bstr .size (1..63)
nai = tstr .regexp "[A-Za-z0-9]+@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)+"
rwxbits = uint .bits rwx

coseHeader = { alg: int }
cosePayload = bstr .cbor coseHeader

coseHeaders = [coseHeader]
coseSeq = bstr .cborseq coseHeaders

foo = ip4 .and nai
fooWithin = ip4 .within nai
bar = foo .ne fooWithin
Expand Down
49 changes: 48 additions & 1 deletion packages/cddl/docs/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Operators in CDDL modify or constrain types, providing additional semantics beyo
In the AST, an operator is represented by the following structure:

```typescript
export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge'
export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' | 'cbor' | 'cborseq'

export interface Operator {
Type: OperatorType
Expand Down Expand Up @@ -35,6 +35,8 @@ CDDL supports the following operators, represented in the AST:
10. `le`: Less than or equal comparison
11. `gt`: Greater than comparison
12. `ge`: Greater than or equal comparison
13. `cbor`: Asserts a byte string, decoded as CBOR, matches the given type (RFC 8610 §3.8.4)
14. `cborseq`: Asserts a byte string, decoded as a sequence of CBOR items, matches the given (array) type (RFC 8610 §3.8.4)

## Operator Location in the AST

Expand Down Expand Up @@ -242,6 +244,51 @@ AST representation:
}
```

### CBOR Operators

The `.cbor` operator asserts that a byte string, once decoded as CBOR, matches
the given type - the standard way to describe a byte string containing nested
CBOR-encoded data, e.g. how COSE (RFC 9052) describes a `COSE_Sign1`'s
protected header or payload field. `.cborseq` is the same idea for a byte
string containing a *sequence* of CBOR items, matched against an array type
(RFC 8610 §3.8.4):

```cddl
coseHeader = { alg: int }
cosePayload = bstr .cbor coseHeader
```

AST representation:

```json
{
"Type": "variable",
"Name": "cosePayload",
"IsChoiceAddition": false,
"PropertyType": [
{
"Type": "bstr",
"Operator": {
"Type": "cbor",
"Value": {
"Type": "group",
"Value": "coseHeader",
"Unwrapped": false
}
}
}
],
"Comments": []
}
```

`.cborseq` produces the identical shape with `"Type": "cborseq"` in place of
`"cbor"`. Like `.size`/`.regexp`/the comparison operators, this is a
validation constraint on the value, not a structural type change - the
`cddl2*` generators all resolve `bstr .cbor X` to their native byte-array
type (`Uint8Array`, `bytes`, `[UInt8]`, `ByteArray`, `byte[]`), ignoring the
operator and its referenced type.

### Comparison Operators

The `.ge`, `.gt`, `.le`, `.lt`, `.eq`, and `.ne` operators specify numerical constraints:
Expand Down
2 changes: 1 addition & 1 deletion packages/cddl/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export type Range = {
Inclusive: boolean
}

export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge'
export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' | 'cbor' | 'cborseq'
export interface Operator {
Type: OperatorType
Value: PropertyType
Expand Down
4 changes: 3 additions & 1 deletion packages/cddl/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {

const NIL_TOKEN: Token = { Type: Tokens.ILLEGAL, Literal: '' }
const DEFAULT_OCCURRENCE: Occurrence = { n: 1, m: 1 } // exactly one time
const OPERATORS: OperatorType[] = ['default', 'size', 'regexp', 'bits', 'and', 'within', 'eq', 'ne', 'lt', 'le', 'gt', 'ge']
const OPERATORS: OperatorType[] = ['default', 'size', 'regexp', 'bits', 'and', 'within', 'eq', 'ne', 'lt', 'le', 'gt', 'ge', 'cbor', 'cborseq']
const OPERATORS_EXPECTING_VALUES: Record<OperatorType, PropertyReferenceType[] | undefined> = {
default: undefined,
size: ['literal', 'range'],
Expand All @@ -27,6 +27,8 @@ const OPERATORS_EXPECTING_VALUES: Record<OperatorType, PropertyReferenceType[] |
le: ['group'],
gt: ['group'],
ge: ['group'],
cbor: ['group'],
cborseq: ['group'],
}

export default class Parser {
Expand Down
81 changes: 81 additions & 0 deletions packages/cddl/tests/__snapshots__/parser.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,87 @@ exports[`parser > can parse operators 1`] = `
],
"Type": "variable",
},
{
"Comments": [],
"IsChoiceAddition": false,
"Name": "coseHeader",
"Properties": [
{
"Comments": [],
"HasCut": true,
"Name": "alg",
"Occurrence": {
"m": 1,
"n": 1,
},
"Type": [
"int",
],
},
],
"Type": "group",
},
{
"Comments": [],
"IsChoiceAddition": false,
"Name": "cosePayload",
"PropertyType": [
{
"Operator": {
"Type": "cbor",
"Value": {
"Type": "group",
"Unwrapped": false,
"Value": "coseHeader",
},
},
"Type": "bstr",
},
],
"Type": "variable",
},
{
"Comments": [],
"Name": "coseHeaders",
"Type": "array",
"Values": [
{
"Comments": [],
"HasCut": false,
"Name": "",
"Occurrence": {
"m": 1,
"n": 1,
},
"Type": [
{
"Type": "group",
"Unwrapped": false,
"Value": "coseHeader",
},
],
},
],
},
{
"Comments": [],
"IsChoiceAddition": false,
"Name": "coseSeq",
"PropertyType": [
{
"Operator": {
"Type": "cborseq",
"Value": {
"Type": "group",
"Unwrapped": false,
"Value": "coseHeaders",
},
},
"Type": "bstr",
},
],
"Type": "variable",
},
{
"Comments": [],
"IsChoiceAddition": false,
Expand Down
47 changes: 47 additions & 0 deletions packages/cddl/tests/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,51 @@ describe('parser', () => {

vi.restoreAllMocks()
})

it('parses the .cbor operator on a map member, referencing another rule as the embedded type', () => {
vi.spyOn(fs, 'readFileSync').mockReturnValue('inner = { a: int }\nouter = { ? parent: bstr .cbor inner }\n')
const p = new Parser('foo.cddl')

const [, outer] = p.parse() as Group[]
expect(outer.Properties).toEqual([{
HasCut: true,
Occurrence: { n: 0, m: 1 },
Name: 'parent',
Type: [{
Type: 'bstr',
Operator: {
Type: 'cbor',
Value: { Type: 'group', Value: 'inner', Unwrapped: false }
}
}],
Comments: []
}])

vi.restoreAllMocks()
})

it('parses the .cborseq operator the same way, on a required member', () => {
// .cborseq's argument must be an array type (RFC 8610 §3.8.4: the decoded
// sequence, taken as an array, is matched against it) - unlike .cbor's inner,
// which matches a single decoded item and can legitimately be a map.
vi.spyOn(fs, 'readFileSync').mockReturnValue('innerList = [int]\nouter = { payload: bstr .cborseq innerList }\n')
const p = new Parser('foo.cddl')

const [, outer] = p.parse() as Group[]
expect(outer.Properties).toEqual([{
HasCut: true,
Occurrence: { n: 1, m: 1 },
Name: 'payload',
Type: [{
Type: 'bstr',
Operator: {
Type: 'cborseq',
Value: { Type: 'group', Value: 'innerList', Unwrapped: false }
}
}],
Comments: []
}])

vi.restoreAllMocks()
})
})
11 changes: 11 additions & 0 deletions packages/cddl2java/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ export function parseType (specType: any): { type: string, isLiteral: boolean }
return { type: 'Float', isLiteral: false };
} else if ((specType[0] === 'string' || specType[0] === 'text')) {
return { type: 'String', isLiteral: false };
} else if (specType[0] === 'bstr' || specType[0] === 'bytes') {
return { type: 'byte[]', isLiteral: false };
} else if (specType[0] === 'null' || specType[0] === 'any') {
return { type: 'Object', isLiteral: false }; // null/any represented as Object in Java
} else if (typeof specType[0] === 'object') {
Expand Down Expand Up @@ -571,6 +573,15 @@ export function parseType (specType: any): { type: string, isLiteral: boolean }
}
}
}

// Any other operator (.cbor, .cborseq, .size, .regexp, .bits, .and, .within,
// .eq, .ne, .lt, .le, .gt, .ge, ...) is a validation constraint on the value,
// not a structural type change - resolve the underlying type and ignore it,
// the same way the other generators already treat these operators.
if (typeof specType[0].Type === 'string') {
return parseType([specType[0].Type]);
}

/* c8 ignore next */ throw new Error(`Unknown operator: ${JSON.stringify(specType[0].Operator)}`)
}
}
Expand Down
34 changes: 34 additions & 0 deletions packages/cddl2java/tests/parseType.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,38 @@ describe('parseType', () => {
expect(parseType(specType)).toEqual({ type: 'Integer', isLiteral: false })
})
})

describe('byte strings', () => {
it('resolves bare bstr/bytes to byte[]', () => {
// was "Unknown" before the fix - bstr/bytes had no primitive-type mapping at all
expect(parseType(['bstr'])).toEqual({ type: 'byte[]', isLiteral: false })
expect(parseType(['bytes'])).toEqual({ type: 'byte[]', isLiteral: false })
})

it('resolves a bstr with an unrecognized operator by ignoring the operator, not throwing', () => {
// .cbor/.cborseq (RFC 8610 §3.8.4) are validation constraints on a byte string,
// not a structural type change - same as every other generator already treats
// .size/.and/.within/etc. Before the fix this threw "Unknown operator". The
// generator never looks at what the reference resolves to, but the names below
// still follow the RFC: .cbor's argument matches a single decoded item (a map
// is fine), .cborseq's matches the sequence taken as an array.
const cborSpecType = [{
Type: 'bstr',
Operator: {
Type: 'cbor',
Value: { Type: 'group', Value: 'inner', Unwrapped: false }
}
}]
expect(parseType(cborSpecType)).toEqual({ type: 'byte[]', isLiteral: false })

const cborseqSpecType = [{
Type: 'bstr',
Operator: {
Type: 'cborseq',
Value: { Type: 'group', Value: 'innerList', Unwrapped: false }
}
}]
expect(parseType(cborseqSpecType)).toEqual({ type: 'byte[]', isLiteral: false })
})
})
})
24 changes: 24 additions & 0 deletions packages/cddl2kotlin/tests/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ describe('transform', () => {
expect(output).toContain('typealias DeviceAddress = String')
})

it.each([
['cbor', 'inner'], // .cbor's argument matches a single decoded item - a map is fine
['cborseq', 'innerList'] // .cborseq's argument matches the sequence taken as an array (RFC 8610 §3.8.4)
] as const)('should ignore the .%s operator and still map bstr to ByteArray', (operatorType, referencedType) => {
// Either way this is a validation constraint on a byte string, not a structural
// type change - same as every other operator (.size, .and, ...); the generator
// never looks at what the reference resolves to.
const assignment: Variable = {
Type: 'variable',
Name: 'payload',
PropertyType: {
Type: 'bstr',
Operator: {
Type: operatorType,
Value: { Type: 'group', Value: referencedType, Unwrapped: false }
}
} as any,
Comments: [],
IsChoiceAddition: false
}
const output = transform([assignment])
expect(output).toContain('typealias Payload = ByteArray')
})

it('should transform a string literal union into an enum class', () => {
const assignment: Variable = {
Type: 'variable',
Expand Down
17 changes: 17 additions & 0 deletions packages/cddl2py/tests/transform_edge_cases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,4 +656,21 @@ describe('transform edge cases', () => {
expect(output).toContain('# leading docs')
expect(output).toContain('Commented = str')
})

it.each([
['cbor', 'inner'], // .cbor's argument matches a single decoded item - a map is fine
['cborseq', 'innerList'] // .cborseq's argument matches the sequence taken as an array (RFC 8610 §3.8.4)
] as const)('should ignore the .%s operator and still map bstr to bytes', (operatorType, referencedType) => {
// Either way this is a validation constraint on a byte string, not a structural
// type change - same as every other operator (.size, .and, ...); the generator
// never looks at what the reference resolves to.
const output = transform([variable('payload', {
Type: 'bstr',
Operator: {
Type: operatorType,
Value: { Type: 'group', Value: referencedType, Unwrapped: false }
}
} as any)])
expect(output).toContain('Payload = bytes')
})
})
24 changes: 24 additions & 0 deletions packages/cddl2swift/tests/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ describe('transform', () => {
expect(output).toContain('public typealias DeviceAddress = String')
})

it.each([
['cbor', 'inner'], // .cbor's argument matches a single decoded item - a map is fine
['cborseq', 'innerList'] // .cborseq's argument matches the sequence taken as an array (RFC 8610 §3.8.4)
] as const)('should ignore the .%s operator and still map bstr to [UInt8]', (operatorType, referencedType) => {
// Either way this is a validation constraint on a byte string, not a structural
// type change - same as every other operator (.size, .and, ...); the generator
// never looks at what the reference resolves to.
const assignment: Variable = {
Type: 'variable',
Name: 'payload',
PropertyType: {
Type: 'bstr',
Operator: {
Type: operatorType,
Value: { Type: 'group', Value: referencedType, Unwrapped: false }
}
} as any,
Comments: [],
IsChoiceAddition: false
}
const output = transform([assignment])
expect(output).toContain('public typealias Payload = [UInt8]')
})

it('should transform a string literal union into a raw value enum', () => {
const assignment: Variable = {
Type: 'variable',
Expand Down
Loading
Loading