Zum Hauptinhalt springen

3. Syntax

In diesem Abschnitt wird die Gesamtsyntax von CDDL dargestellt, zusammen mit einigen Beispielen, die nur die Syntax veranschaulichen. (Die Definition versucht nicht, übermäßig formal zu sein; siehe Anhang B für Details.)

3.1. Allgemeine Konventionen

Die grundlegende Syntax ist von ABNF [RFC5234] inspiriert, mit den folgenden Merkmalen:

  • Regeln, unabhängig davon, ob sie Gruppen oder Typen definieren, werden mit einem Namen definiert, gefolgt von einem Gleichheitszeichen "=" und der eigentlichen Definition gemäß den jeweiligen syntaktischen Regeln dieser Definition.

  • Ein Name kann aus beliebigen Zeichen der Menge {"A" bis "Z", "a" bis "z", "0" bis "9", "", "-", "@", ".", "$"} bestehen, beginnend mit einem alphabetischen Zeichen (einschließlich "@", "", "$") und endend mit einem solchen Zeichen oder einer Ziffer.

    • Namen unterscheiden zwischen Groß- und Kleinschreibung.
    • Es wird bevorzugt, einen Namen mit einem Kleinbuchstaben zu beginnen.
    • Der Bindestrich wird dem Unterstrich vorgezogen (außer in einem "Bareword" (Abschnitt 3.5.1), wo die Semantik tatsächlich einen Unterstrich erfordern kann).
    • Der Punkt kann für größere Spezifikationen nützlich sein, um eine Modulstruktur auszudrücken (wie in "tcp.throughput" vs. "udp.throughput").
    • Eine Reihe von Namen sind im CDDL-Präludium vordefiniert, wie in Anhang D aufgeführt.
    • Regelnamen (Typen oder Gruppen) erscheinen nicht in der eigentlichen CBOR-Kodierung, aber Namen, die als "Barewords" in Mitgliederschlüsseln verwendet werden, tun dies.
  • Kommentare beginnen mit einem ";"-Zeichen (Semikolon) und enden am Ende einer Zeile (LF oder CRLF).

  • Außerhalb von Zeichenfolgen wird Leerraum (Leerzeichen, Zeilenumbrüche und Kommentare) verwendet, um syntaktische Elemente zur besseren Lesbarkeit zu trennen (und um Bezeichner, Bereichsoperatoren oder Zahlen zu trennen, die aufeinander folgen); ansonsten ist er völlig optional.

  • Hexadezimalzahlen wird "0x" (ohne Anführungszeichen) vorangestellt und sie unterscheiden nicht zwischen Groß- und Kleinschreibung. Ebenso wird Binärzahlen "0b" vorangestellt.

  • Textzeichenfolgen werden von doppelten Anführungszeichen '"' umschlossen. Sie folgen den Konventionen für Zeichenfolgen, wie sie in Abschnitt 7 von [RFC8259] definiert sind. (ABNF-Benutzer sollten beachten, dass es in CDDL keine Unterstützung für das Konzept der Groß-/Kleinschreibung in Textzeichenfolgen gibt; falls erforderlich, können reguläre Ausdrücke verwendet werden (Abschnitt 3.8.3).)

  • Bytezeichenfolgen werden von einfachen Anführungszeichen "'" umschlossen und können mit "h" oder "b64" vorangestellt werden. Wenn kein Präfix vorhanden ist, wird die Zeichenfolge wie eine Textzeichenfolge interpretiert, außer dass einfache Anführungszeichen maskiert werden müssen und die resultierenden UTF-8-Bytes als Bytezeichenfolge (Haupttyp 2) markiert werden. Wenn sie mit "h" oder "b64" vorangestellt ist, wird die Zeichenfolge als Folge von Paaren von Hexadezimalziffern (base16; siehe Abschnitt 8 von [RFC4648]) bzw. als base64(url)-Zeichenfolge (Abschnitt 4 oder Abschnitt 5 von [RFC4648]) interpretiert (wie bei der Diagnosenotation in Abschnitt 6 von [RFC7049]; vgl. Anhang G.2); jeglicher Leerraum innerhalb der Zeichenfolge (einschließlich Kommentare) wird im präfigierten Fall ignoriert.

  • CDDL verwendet UTF-8 [RFC3629] für seine Kodierung. Die Verarbeitung von CDDL beinhaltet keine Unicode-Normalisierungsprozesse.

Beispiel:

; Dies ist ein Kommentar
person = { g }

g = (
"name": tstr,
age: int, ; "age" ist ein Bareword
)

3.2. Vorkommen (Occurrence)

Ein optionaler Vorkommensindikator kann vor einem Gruppeneintrag angegeben werden. Er ist entweder (1) eines der Zeichen "?" (optional), "" (null oder mehr) oder "+" (eins oder mehr) oder (2) von der Form nm, wobei n und m optionale vorzeichenlose Ganzzahlen sind und n die untere Grenze (Standard 0) und m die obere Grenze (Standard keine Grenze) der Vorkommen ist.

Wenn kein Vorkommensindikator angegeben ist, muss der Gruppeneintrag genau einmal vorkommen (als ob 1*1 angegeben wäre). Ein Gruppeneintrag mit einem Vorkommensindikator entspricht Folgen von Name/Wert-Paaren, die durch Verketten einer Anzahl von Folgen zusammengesetzt sind, die der Basisgruppeneintrag entspricht, wobei die Anzahl durch den Vorkommensindikator zulässig sein muss.

Beachten Sie, dass CDDL, außerhalb von Direktiven/Annotationen, die möglicherweise definiert werden könnten, keine Vorschrift darüber macht, ob Arrays oder Maps eine Kodierung mit bestimmter oder unbestimmter Länge verwenden. Das heißt, es gibt keine Korrelation zwischen dem "Offenlassen" der Größe eines Arrays in der Spezifikation und der Tatsache, dass es dann mit bestimmter oder unbestimmter Länge ausgetauscht wird.

Bitte beachten Sie auch, dass CDDL Flexibilität beschreiben kann, die das Datenmodell der Zieldarstellung nicht hat. Dies ist für JSON ziemlich offensichtlich, ist aber auch für CBOR relevant:

apartment = {
kitchen: size,
* bedroom: size,
}
size = float ; in m2

Die vorherige Spezifikation bedeutet nicht, dass CBOR geändert wird, um die Verwendung des Schlüssels "bedroom" mehr als einmal zu erlauben. Mit anderen Worten, aufgrund der durch das Datenmodell auferlegten Einschränkungen wird die dritte Zeile so ziemlich zu:

? bedroom: size,

(Vorkommensindikatoren jenseits von eins sind in Maps für Gruppen, die eine Vielzahl von Schlüsseln zulassen, immer noch nützlich.)

3.3. Vordefinierte Namen für Typen

CDDL definiert eine Reihe von Namen vor. Dieser Unterabschnitt fasst diese Namen zusammen, aber bitte sehen Sie Anhang D für die genauen Definitionen.

Die folgenden Schlüsselwörter für primitive Datentypen sind definiert:

"bool": Boolescher Wert (Haupttyp 7, Zusatzinformation 20 oder 21).

"uint": Eine vorzeichenlose Ganzzahl (Haupttyp 0).

"nint": Eine negative Ganzzahl (Haupttyp 1).

"int": Eine vorzeichenlose Ganzzahl oder eine negative Ganzzahl.

"float16": Eine Zahl, die als Gleitkommazahl mit halber Genauigkeit darstellbar ist [IEEE754] (Haupttyp 7, Zusatzinformation 25).

"float32": Eine Zahl, die als Gleitkommazahl mit einfacher Genauigkeit darstellbar ist [IEEE754] (Haupttyp 7, Zusatzinformation 26).

"float64": Eine Zahl, die als Gleitkommazahl mit doppelter Genauigkeit darstellbar ist [IEEE754] (Haupttyp 7, Zusatzinformation 27).

"float": Eines von float16, float32 oder float64.

"bstr" oder "bytes": Eine Bytezeichenfolge (Haupttyp 2).

"tstr" oder "text": Textzeichenfolge (Haupttyp 3).

(Beachten Sie, dass es keine vordefinierten Namen für Arrays oder Maps gibt; diese werden mit der unten angegebenen Syntax definiert.)

Darüber hinaus sind im Präludium eine Reihe von Typen definiert, die mit CBOR-Tags verbunden sind, wie "tdate", "bigint", "regexp" usw.

3.4. Arrays

Array-Definitionen umschließen eine Gruppe mit eckigen Klammern.

Für jeden Eintrag ist ein Vorkommensindikator wie in Abschnitt 3.2 angegeben zulässig.

Zum Beispiel:

unlimited-people = [* person]
one-or-two-people = [1*2 person]
at-least-two-people = [2* person]
person = (
name: tstr,
age: uint,
)

Die Gruppe "person" ist so definiert, dass ihre Wiederholung im Array jedes Mal abwechselnde Namen und Alter erzeugt, sodass dies vier gültige Werte für ein Datenelement vom Typ "unlimited-people" sind:

["roundlet", 1047, "psychurgy", 2204, "extrarhythmical", 2231]
[]
["aluminize", 212, "climograph", 4124]
["penintime", 1513, "endocarditis", 4084, "impermeator", 1669,
"coextension", 865]

3.5. Maps

Die Syntax zur Spezifikation von Maps verdient besondere Aufmerksamkeit sowie eine Reihe von Optimierungen und Annehmlichkeiten, da sie wahrscheinlich der Schwerpunkt vieler Spezifikationen sein wird, die CDDL verwenden. Während die Syntax nicht streng zwischen der Struct- und der Tabellennutzung von Maps unterscheidet, geht sie speziell auf jede von ihnen ein.

Aber zuerst wollen wir ein Merkmal von CBOR wiederholen, das es von JSON geerbt hat: Die Schlüssel/Wert-Paare in CBOR-Maps haben keine feste Reihenfolge. (Man könnte sich Situationen vorstellen, in denen das Festlegen der Reihenfolge von Nutzen sein könnte. Beispielsweise könnte ein Decoder nach Werten suchen, die mit den ganzzahligen Schlüsseln 1, 3 und 7 verbunden sind. Wenn die Reihenfolge festgelegt wäre und der Decoder den Schlüssel 4 findet, ohne den Schlüssel 3 gefunden zu haben, könnte er schließen, dass Schlüssel 3 nicht verfügbar ist, ohne kompliziertere Buchführung durchzuführen. Leider unterstützen weder JSON noch CBOR dies, daher wurde auch kein Versuch unternommen, dies in CDDL zu unterstützen.)

3.5.1. Strukturen (Structs)

Die "Struct"-Nutzung von Maps ähnelt der Art und Weise, wie JSON-Objekte in vielen JSON-Anwendungen verwendet werden.

Eine Map wird auf die gleiche Weise definiert wie ein Array (siehe Abschnitt 3.4), außer dass geschweifte Klammern "{}" anstelle von eckigen Klammern "[]" verwendet werden.

Ein Vorkommensindikator wie in Abschnitt 3.2 angegeben ist für jeden Gruppeneintrag zulässig.

Das Folgende ist ein Beispiel für einen Datensatz mit einer eingebetteten Struktur:

Geography = [
city : tstr,
gpsCoordinates : GpsCoordinates,
]

GpsCoordinates = {
longitude : uint, ; Grad, skaliert mit 10^7
latitude : uint, ; Grad, skaliert mit 10^7
}

Bei der Kodierung wird der Geography-Datensatz unter Verwendung eines CBOR-Arrays mit zwei Mitgliedern kodiert (die Schlüssel für die Gruppeneinträge werden ignoriert), während die GpsCoordinates-Struktur als CBOR-Map mit zwei Schlüssel/Wert-Paaren kodiert wird.

In einer Struktur verwendete Typen können in separaten Regeln oder einfach an Ort und Stelle definiert werden (möglicherweise in Klammern gesetzt, wie z. B. für Auswahlen). Zum Beispiel:

located-samples = {
sample-point: int,
samples: [+ float],
}

wobei "located-samples" der Datentyp ist, der bei Bezugnahme auf das Struct verwendet werden soll, und "sample-point" und "samples" die zu verwendenden Schlüssel sind. Dies ist eigentlich ein vollständiges Beispiel: Ein Bezeichner, gefolgt von einem Doppelpunkt, kann direkt als Textzeichenfolge für einen Mitgliederschlüssel verwendet werden (wir sprechen von einem "Bareword"-Mitgliederschlüssel), ebenso wie eine Zeichenfolge in doppelten Anführungszeichen oder eine Zahl. (Wenn andere Typen – insbesondere Typen, die mehr als einen Wert enthalten – als Typen von Schlüsseln verwendet werden, folgt ein Doppelpfeil darauf; siehe unten.)

Wenn ein Textzeichenfolgenschlüssel nicht der Syntax für einen Bezeichner entspricht (oder wenn der Spezifizierer einfach lieber doppelte Anführungszeichen verwendet), kann die Textzeichenfolgensyntax auch an der Position des Mitgliederschlüssels verwendet werden, gefolgt von einem Doppelpunkt. Das obige Beispiel hätte daher mit zitierten Zeichenfolgen an den Positionen der Mitgliederschlüssel geschrieben werden können.

Allgemeiner können Typen, die auf andere Weise als die für die oben beschriebenen Fälle aufgeführten spezifiziert sind, an einer Schlüsseltypposition verwendet werden, indem ihnen ein Doppelpfeil folgt – insbesondere ist der Doppelpfeil erforderlich, wenn ein Typ durch einen Bezeichner benannt ist (der, wenn er von einem Doppelpunkt gefolgt würde, als "Bareword" interpretiert und in eine Textzeichenfolge umgewandelt würde). Eine ltereale Textzeichenfolge führt auch zu einem Typ (der nur einen einzigen Wert enthält – die gegebene Zeichenfolge), daher ist eine andere Form für dieses Beispiel:

located-samples = {
"sample-point" => int,
"samples" => [+ float],
}

Siehe Abschnitt 3.5.4 unten, wie die hier beschriebene Doppelpunkt-Abkürzung (":") auch einige implizite Semantik hinzufügt.

Ein besserer Weg, die Verwendung des Doppelpfeils zu demonstrieren, könnte sein:

located-samples = {
sample-point: int,
samples: [+ float],
* equipment-type => equipment-tolerances,
}
equipment-type = [name: tstr, manufacturer: tstr]
equipment-tolerances = [+ [float, float]]

Das folgende Beispiel definiert ein Struct mit optionalen Einträgen: Anzeigename (als Textzeichenfolge), die Namenskomponenten Vorname und Nachname (als Textzeichenfolgen) und Altersinformationen (als vorzeichenlose Ganzzahl).

PersonalData = {
? displayName: tstr,
NameComponents,
? age: uint,
}

NameComponents = (
? firstName: tstr,
? familyName: tstr,
)

Beachten Sie, dass die Gruppendefinition für NameComponents keine weitere Map generiert; stattdessen befinden sich alle vier Schlüssel direkt in dem von PersonalData erstellten Struct.

In diesem Beispiel sind alle Schlüssel/Wert-Paare aus Sicht von CDDL optional. Ohne Vorkommensindikator ist ein Eintrag obligatorisch.

Wenn das Hinzufügen weiterer Einträge gewünscht ist, die nicht durch die aktuelle Spezifikation spezifiziert sind, kann man diese Möglichkeit explizit hinzufügen:

PersonalData = {
? displayName: tstr,
NameComponents,
? age: uint,
* tstr => any
}

NameComponents = (
? firstName: tstr,
? familyName: tstr,
)

Abbildung 7: Persönliche Daten: Beispiel für Erweiterbarkeit

Das in Anhang F beschriebene CDDL-Tool generierte Folgendes als eine akzeptable Instanz für diese Spezifikation:

{"familyName": "agust", "antiforeignism": "pretzel",
"springbuck": "illuminatingly", "exuviae": "ephemeris",
"kilometrage": "frogfish"}

(Siehe Abschnitt 3.9 für eine Möglichkeit, einen Erweiterungspunkt explizit zu identifizieren.)

3.5.2. Tabellen (Tables)

Eine Tabelle kann spezifiziert werden, indem eine Map mit Einträgen definiert wird, bei denen der Schlüsseltyp mehr als nur einen einzelnen Wert zulässt; zum Beispiel:

square-roots = {* x => y}
x = int
y = float

Hier hat der Schlüssel in jedem Schlüssel/Wert-Paar den Datentyp x (definiert als int) und der Wert hat den Datentyp y (definiert als float).

Wenn die Spezifikation x oder y nicht einschränken muss (d. h. die Anwendung kann pro Eintrag frei wählen), kann sie durch den vordefinierten Namen "any" ersetzt werden.

Als weiteres Beispiel könnte Folgendes als Umrechnungstabelle verwendet werden, die von einer Ganzzahl oder einem Fließkommawert in eine Zeichenfolge konvertiert:

tostring = {* mynumber => tstr}
mynumber = int / float

3.5.3. Nicht-deterministische Reihenfolge

Während die Art und Weise, wie Arrays abgeglichen werden, vollständig durch den PEG-Formalismus bestimmt wird (siehe Anhang A), ist der Abgleich für Maps komplizierter, da Maps keine inhärente Reihenfolge haben. Für jedes Kandidaten-Name/Wert-Paar, das der PEG-Algorithmus versuchen würde, wird ein passendes Mitglied aus der gesamten Map ausgewählt. Bei bestimmten Gruppenausdrücken kann mehr als ein Mitglied in der Map übereinstimmen. Meistens ist dies belanglos, da der Gruppenausdruck dazu neigt, alle Übereinstimmungen zu verbrauchen:

labeled-values = {
? fritz: number,
* label => value
}
label = text
value = number

Wenn hier ein Mitglied mit dem Schlüssel "fritz" vorhanden ist, wird dieses vom ersten Eintrag der Gruppe ausgewählt; alle verbleibenden Text/Zahlen-Mitglieder werden vom zweiten Eintrag ausgewählt (und wenn etwas Nicht-Ausgewähltes übrig bleibt, stimmt die Map nicht überein).

Es ist jedoch möglich, Gruppenausdrücke zu konstruieren, bei denen das, was tatsächlich ausgewählt wird, unbestimmt ist, aber eine Rolle spielt:

do-not-do-this = {
int => int,
int => 6,
}

Wenn dieser Ausdruck gegen "{3: 5, 4: 6}" abgeglichen wird, könnte der erste Gruppeneintrag das "3: 5" auswählen und "4: 6" für den Abgleich des zweiten übrig lassen. Oder er könnte "4: 6" auswählen und nichts für den zweiten Eintrag übrig lassen. Dieser pathologische Nicht-Determinismus wird dadurch verursacht, dass "allgemeiner" vor "spezifischer" angegeben wird und dass eine allgemeine Regel vorhanden ist, die nur eine Teilmenge der Map-Schlüssel/Wert-Paare verbraucht, die sie abgleichen kann – beides tritt in realen Spezifikationen von Maps eher nicht auf. Zum Zeitpunkt des Schreibens können CDDL-Tools solche Fälle nicht automatisch erkennen, und für die vorliegende Version der CDDL-Spezifikation wird der Spezifikationsschreiber einfach dringend gebeten, keine pathologisch nicht-deterministischen Spezifikationen zu schreiben.

(Der aufmerksame Leser wird an das erinnert, was in der Standard Generalized Markup Language (SGML) als "zweideutige Inhaltsmodelle" und in XML als "nicht-deterministische Inhaltsmodelle" bezeichnet wurde. Dieses Problem hängt mit dem hier beschriebenen zusammen, aber das Problem wird hier speziell durch das Fehlen einer Reihenfolge in Maps verursacht, etwas, womit sich die XML-Schemsprachen nicht auseinandersetzen müssen. Beachten Sie, dass das "interleave"-Muster von RELAX NG das Fehlen einer Reihenfolge auf der Spezifikationsseite explizit behandelt, während die Instanzen in XML immer eine bestimmte Reihenfolge haben.)

3.5.4. Schnitte in Maps (Cuts)

Das oben diskutierte Erweiterbarkeits-Idiom für Structs hat ein Problem:

extensible-map-example = {
? "optional-key" => int,
* tstr => any
}

In diesem Beispiel gibt es einen optionalen Schlüssel "optional-key", der, wenn vorhanden, auf eine Ganzzahl abgebildet wird. Es gibt auch einen Platzhalter für alle zukünftigen Ergänzungen.

Leider stimmt das Datenelement

{ "optional-key": "nonsense" }

mit dieser Spezifikation überein: Während der erste Eintrag der Gruppe nicht übereinstimmt, tut dies der zweite (der Platzhalter). Dies kann durchaus wünschenswert sein (z. B. wenn eine zukünftige Erweiterung den Typ von "optional-key" erweitern darf), aber in vielen Fällen ist es das nicht.

In Erwartung einer allgemeineren potenziellen Funktion namens "Schnitte" (Cuts) erlaubt CDDL das Einfügen eines Schnitts "^" in die Definition des Map-Eintrags:

extensible-map-example = {
? "optional-key" ^ => int,
* tstr => any
}

Ein Schnitt an dieser Position bedeutet, dass, sobald der Mitgliederschlüssel mit dem Namensteil eines Eintrags übereinstimmt, der einen Schnitt trägt, andere potenzielle Übereinstimmungen für den Schlüssel des Mitglieds, die in späteren Einträgen in der Gruppe der Map auftreten, nicht mehr zulässig sind. Mit anderen Worten, wenn ein Gruppeneintrag ein Schlüssel/Wert-Paar basierend auf nur einem passenden Schlüssel auswählen würde, "sperrt" er die Auswahl – diese Regel gilt unabhängig davon, ob der Wert ebenfalls übereinstimmt. Wenn er also nicht übereinstimmt, schlägt der Abgleich der gesamten Map fehl. Zusammenfassend lässt sich sagen, dass das obige Beispiel nicht mehr mit der mit dem Schnitt modifizierten Spezifikation übereinstimmt.

Da der Wunsch nach dieser Art von exklusivem Abgleich so häufig ist, ist die Abkürzung ":" tatsächlich so definiert, dass sie die Schnitt-Semantik einschließt. Das vorhergehende Beispiel (einschließlich des Schnitts) kann also einfacher geschrieben werden als:

extensible-map-example = {
? "optional-key": int,
* tstr => any
}

oder noch kürzer, indem ein Bareword für den Schlüssel verwendet wird:

extensible-map-example = {
? optional-key: int,
* tstr => any
}

3.6. Tags

Ein Typ kann ein CBOR-Tag (Haupttyp 6) verwenden, indem er die Repräsentationstyp-Notation verwendet und #6.nnn(type) angibt, wobei nnn eine vorzeichenlose Ganzzahl ist, die die Tag-Nummer angibt, und "type" der Typ des getaggten Datenelements ist.

Zum Beispiel definiert die folgende Zeile aus dem CDDL-Präludium (Anhang D) "biguint" als Typnamen für eine vorzeichenlose Bignum N:

biguint = #6.2(bstr)

Die durch [RFC7049] definierten Tags sind im Präludium enthalten. Zusätzliche Tags, die seit dem Schreiben von [RFC7049] registriert wurden, müssen einer CDDL-Spezifikation nach Bedarf hinzugefügt werden; z. B. könnte ein binäres Tag für einen Universally Unique Identifier (UUID) in einer Spezifikation als "buuid" referenziert werden, nachdem

buuid = #6.37(bstr)

definiert wurde.

Im folgenden Beispiel ist die Verwendung von Tag 32 für URIs optional:

my_uri = #6.32(tstr) / tstr

3.7. Auspacken (Unwrapping)

Die Gruppe, die verwendet wird, um eine Map oder ein Array zu definieren, kann oft in der Definition einer anderen Map oder eines anderen Arrays wiederverwendet werden. Ebenso trägt ein als Tag definierter Typ ein internes Datenelement, auf das man Bezug nehmen möchte. In diesen Fällen ist es zweckmäßig, einfach den Namen des Map-, Array- oder Tag-Typs als Handle für die darin definierte Gruppe oder den darin definierten Typ zu verwenden.

Der "unwrap"-Operator (geschrieben durch Voranstellen eines Tilde-Zeichens "~" vor einen Namen) kann verwendet werden, um den für einen Namen definierten Typ um eine Ebene zu entfernen, wodurch die zugrunde liegende Gruppe (für Maps und Arrays) oder der Typ (für Tags) offengelegt wird.

Beispielsweise möchte eine Anwendung möglicherweise einen Basis-Header und einen erweiterten Header definieren. Ohne Auspacken könnte dies wie folgt geschehen:

basic-header-group = (
field1: int,
field2: text,
)

basic-header = [ basic-header-group ]

advanced-header = [
basic-header-group,
field3: bytes,
field4: number, ; wie im getaggten Typ "time"
]

Auspacken vereinfacht dies zu:

basic-header = [
field1: int,
field2: text,
]

advanced-header = [
~basic-header,
field3: bytes,
field4: ~time,
]

(Beachten Sie, dass das Weglassen des ersten Unwrap-Operators im letzteren Beispiel dazu führen würde, dass der basic-header in seinem eigenen Array innerhalb des advanced-header verschachtelt wird, während mit dem ausgepackten basic-header die Definition der Gruppe innerhalb des basic-header im Wesentlichen innerhalb des advanced-header wiederholt wird, was zu einem einzigen Array führt. Dies kann für verschiedene Anwendungen verwendet werden, die in Programmiersprachen oft durch Vererbung gelöst werden. Der Effekt des Auspackens kann auch als "Einfädeln" der Gruppe oder des Typs innerhalb des referenzierten Typs beschrieben werden, was das fadenartige "~"-Zeichen nahelegte.)

3.8. Steuerungen (Controls)

Eine Steuerung ermöglicht es, einen Ziel-Typ über einen Steuerungsoperator mit einem Controller-Typ in Beziehung zu setzen.

Die Syntax für einen Steuerungstyp ist "target .control-operator controller", wobei Steuerungsoperatoren spezielle Bezeichner sind, denen ein Punkt vorangestellt ist. (Beachten Sie, dass target oder controller möglicherweise in Klammern gesetzt werden müssen.)

Zu diesem Zeitpunkt sind eine Reihe von Steuerungsoperatoren definiert. Weitere Steuerungsoperatoren können durch neue Versionen dieser Spezifikation oder durch Registrierung gemäß den Verfahren in Abschnitt 6.1 definiert werden.

3.8.1. Steuerungsoperator .size

Eine ".size"-Steuerung steuert die Größe des Ziels in Bytes durch den Steuerungstyp. Die Steuerung ist für Text- und Bytezeichenfolgen definiert, wo sie direkt die Anzahl der Bytes in der Zeichenfolge steuert. Sie ist auch für vorzeichenlose Ganzzahlen definiert (siehe unten). Abbildung 8 zeigt eine Beispielverwendung für Bytezeichenfolgen.

full-address = [[+ label], ip4, ip6]
ip4 = bstr .size 4
ip6 = bstr .size 16
label = bstr .size (1..63)

Abbildung 8: Steuerung für Größe in Bytes

Wenn sie auf eine vorzeichenlose Ganzzahl angewendet wird, schränkt die ".size"-Steuerung den Bereich dieser Ganzzahl ein, indem sie eine maximale Anzahl von Bytes angibt, die in einer Computerrepräsentation dieser vorzeichenlosen Ganzzahl benötigt werden sollten. Mit anderen Worten, "uint .size N" ist äquivalent zu "0...BYTES_N", wobei BYTES_N == 256**N.

audio_sample = uint .size 3 ; 24-Bit, äquivalent zu 0...16777216

Abbildung 9: Steuerung für Ganzzahlgröße in Bytes

Beachten Sie, dass diese Steuerung, wie bei Werteinschränkungen in CDDL, keine Repräsentationsbeschränkung ist; eine Zahl, die in weniger Bytes passt, kann immer noch in dieser Form dargestellt werden, und eine ineffiziente Implementierung könnte eine längere Form verwenden (es sei denn, dies wird durch einige Formatbeschränkungen außerhalb von CDDL eingeschränkt, wie z. B. die Regeln in Abschnitt 3.9 von [RFC7049]).

3.8.2. Steuerungsoperator .bits

Eine ".bits"-Steuerung für eine Bytezeichenfolge gibt an, dass im Ziel nur die Bits gesetzt werden dürfen, die durch eine Zahl im Steuerungstyp nummeriert sind. (Bits werden wie üblich gezählt, wobei Bitnummer "n", die in "str" gesetzt ist, bedeutet, dass "(str[n >> 3] & (1 << (n & 7))) != 0".) Ebenso gibt eine ".bits"-Steuerung für eine vorzeichenlose Ganzzahl "i" an, dass für alle vorzeichenlosen Ganzzahlen "n", bei denen "(i & (1 << n)) != 0", "n" im Steuerungstyp enthalten sein muss.

tcpflagbytes = bstr .bits flags
flags = &(
fin: 8,
syn: 9,
rst: 10,
psh: 11,
ack: 12,
urg: 13,
ece: 14,
cwr: 15,
ns: 0,
) / (4..7) ; Datenoffset-Bits

rwxbits = uint .bits rwx
rwx = &(r: 2, w: 1, x: 0)

Abbildung 10: Steuerung dafür, welche Bits gesetzt werden können

Das in Anhang F beschriebene CDDL-Tool generiert die folgenden zehn Beispielinstanzen für "tcpflagbytes":

h'906d' h'01fc' h'8145' h'01b7' h'013d' h'409f' h'018e' h'c05f'
h'01fa' h'01fe'

Diese Beispiele veranschaulichen nicht, dass die obige CDDL-Spezifikation nicht explizit eine Größe von zwei Bytes angibt: Eine gültige All-Clear-Instanz von Flag-Bytes könnte auch "h''" oder "h'00'" oder sogar "h'000000'" sein.

3.8.3. Steuerungsoperator .regexp

Eine ".regexp"-Steuerung gibt an, dass die als Ziel angegebene Textzeichenfolge mit dem XML Schema Definition (XSD) regulären Ausdruck übereinstimmen muss, der als Wert im Steuerungstyp angegeben ist. XSD reguläre Ausdrücke sind in Anhang F von [W3C.REC-xmlschema-2-20041028] definiert.

nai = tstr .regexp "[A-Za-z0-9]+@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)+"

Abbildung 11: Steuerung mit einem XSD-Regexp

Ein Beispiel, das mit diesem regulären Ausdruck übereinstimmt:

3.8.3.1. Nutzungsüberlegungen

Beachten Sie, dass XSD reguläre Ausdrücke nicht die üblichen \x oder \u Escapes für die hexadezimale Darstellung von Bytes oder Unicode-Codepunkten unterstützen. In CDDL sind die XSD regulären Ausdrücke jedoch in Textzeichenfolgen enthalten, deren Literalschreibweise \u Escapes bietet; dies sollte für die meisten Anwendungen ausreichen, die reguläre Ausdrücke für Textzeichenfolgen verwenden. (Beachten Sie, dass dies auch bedeutet, dass es eine Ebene der Zeichenfolgenmaskierung gibt, bevor die XSD-Maskierungsregeln angewendet werden.)

XSD reguläre Ausdrücke unterstützen die Subtraktion von Zeichenklassen, eine Funktion, die in Bibliotheken für reguläre Ausdrücke oft nicht zu finden ist; Spezifikationsschreiber möchten diese Funktion möglicherweise sparsam verwenden. Ähnliche Überlegungen gelten für Unicode-Zeichenklassen; wo diese verwendet werden, SOLLTE die Spezifikation, die CDDL verwendet, angeben, welche Unicode-Versionen adressiert werden.

Andere Überraschungen für seltene Benutzer von XSD regulären Ausdrücken können Folgendes umfassen:

  • Keine direkte Unterstützung für Groß-/Kleinschreibung. Während die Unabhängigkeit von Groß-/Kleinschreibung im Protokolldesign größtenteils aus der Mode gekommen ist, wird sie manchmal benötigt und muss dann manuell als "[Cc][Aa][Ss][Ee]" ausgedrückt werden.

  • Die Unterstützung für beliebte Zeichenklassen wie \w und \d basiert auf Unicode-Zeichereigenschaften; dies ist oft nicht das, was in einem ASCII-basierten Protokoll gewünscht wird und kann daher zu Überraschungen führen. (\s und \S haben ihre konventionelleren Bedeutungen, und "." entspricht jedem Zeichen außer den Zeilenendezeichen \r oder \n.)

3.8.3.2. Diskussion

In der Programmiergemeinschaft werden viele Varianten von regulären Ausdrücken verwendet. Beispielsweise sind Perl-Compatible Regular Expressions (PCREs) weit verbreitet und wahrscheinlich nützlicher als XSD reguläre Ausdrücke. Es gibt jedoch keine normative Referenz für PCREs, die im vorliegenden Dokument verwendet werden könnte. Stattdessen entscheiden wir uns vorerst für XSD reguläre Ausdrücke. Es gibt einen Präzedenzfall für diese Wahl in der IETF, z. B. in YANG [RFC7950].

Beachten Sie, dass CDDL Steuerungen als seinen Haupterweiterungspunkt verwendet. Dies schafft die Möglichkeit, bei Bedarf weitere Formate für reguläre Ausdrücke zusätzlich zu dem hier referenzierten hinzuzufügen. Als Beispiel ist ein Vorschlag für eine ".pcre"-Steuerung in [CDDL-Freezer] definiert.

3.8.4. Steuerungsoperatoren .cbor und .cborseq

Eine ".cbor"-Steuerung für eine Bytezeichenfolge gibt an, dass die Bytezeichenfolge ein CBOR-kodiertes Datenelement trägt. Dekodiert entspricht das Datenelement dem Typ, der als Argument auf der rechten Seite angegeben ist (type1 im folgenden Beispiel).

bytes .cbor type1

Ebenso gibt eine ".cborseq"-Steuerung für eine Bytezeichenfolge an, dass die Bytezeichenfolge eine Folge von CBOR-kodierten Datenelementen trägt. Wenn die Datenelemente als Array genommen werden, entspricht das Array dem Typ, der als Argument auf der rechten Seite angegeben ist (type2 im folgenden Beispiel).

bytes .cborseq type2

(Die Konvertierung der kodierten Sequenz in ein Array kann beispielsweise dadurch erfolgen, dass die Bytezeichenfolge zwischen den beiden Bytes 0x9f und 0xff eingewickelt wird und die eingewickelte Bytezeichenfolge als CBOR-kodiertes Datenelement dekodiert wird.)

3.8.5. Steuerungsoperatoren .within und .and

Eine ".and"-Steuerung für einen Typ gibt an, dass das Datenelement sowohl mit dem linken Typ als auch mit dem rechten Typ übereinstimmt. (Formal ist der resultierende Typ die Schnittmenge der beiden angegebenen Typen.)

type1 .and type2

Eine Variante der ".and"-Steuerung ist die ".within"-Steuerung, die eine zusätzliche Absicht ausdrückt: Der linke Typ soll eine Teilmenge des rechten Typs sein.

type1 .within type2

Während beide Formen die identische formale Semantik (Schnittmenge) haben, ist die Absicht der ".within"-Form, dass die rechte Seite eine Anleitung für die auf der linken Seite zulässigen Typen gibt, was typischerweise ein Socket ist (Abschnitt 3.9):

message = $message .within message-structure
message-structure = [message_type, *message_option]
message_type = 0..255
message_option = any

$message /= [3, dough: text, topping: [* text]]
$message /= [4, noodles: text, sauce: text, parmesan: bool]

Für ".within" könnte ein Tool einen Fehler melden, wenn type1 Datenelemente zulässt, die von type2 nicht zugelassen werden. Im Gegensatz dazu gibt es bei ".and" keine Erwartung, dass type1 bereits eine Teilmenge von type2 ist.

3.8.6. Steuerungsoperatoren .lt, .le, .gt, .ge, .eq, .ne und .default

Die Steuerungen .lt, .le, .gt, .ge, .eq und .ne spezifizieren eine Einschränkung für den linken Typ, ein Wert zu sein, der kleiner als, kleiner oder gleich, größer als, größer oder gleich, gleich oder ungleich einem Wert ist, der als rechter Typ angegeben ist (der nur diesen einen Wert enthält). In der vorliegenden Spezifikation sind die ersten vier Steuerungen (.lt, .le, .gt und .ge) nur für numerische Typen definiert, da diese eine natürliche Ordnungsbeziehung haben.

speed = number .ge 0  ; Einheit: m/s

.ne und .eq sind sowohl für numerische Werte als auch für Werte anderer Typen definiert. Wenn einer der Werte kein numerischer Typ ist, wird die Gleichheit wie folgt bestimmt: Textzeichenfolgen sind gleich (erfüllen .eq / erfüllen nicht .ne), wenn sie byteweise identisch sind; das Gleiche gilt für Bytezeichenfolgen. Arrays sind gleich, wenn sie die gleiche Anzahl von Elementen haben, die alle paarweise in der Reihenfolge zwischen den Arrays gleich sind. Maps sind gleich, wenn sie die gleiche Anzahl von Schlüssel/Wert-Paaren haben und es eine paarweise Gleichheit zwischen den Schlüssel/Wert-Paaren zwischen den beiden Maps gibt. Getaggte Werte sind gleich, wenn sie beide das gleiche Tag haben und die Werte gleich sind. Werte einfacher Typen stimmen überein, wenn sie die gleichen Werte sind. Numerische Typen, die in Arrays, Maps oder getaggten Werten vorkommen, sind gleich, wenn ihr numerischer Wert gleich ist und sie beide Ganzzahlen oder beide Gleitkommazahlen sind. Alle anderen Fälle sind ungleich (z. B. Vergleich einer Textzeichenfolge mit einer Bytezeichenfolge).

Eine Variante der ".ne"-Steuerung ist die ".default"-Steuerung, die eine zusätzliche Absicht ausdrückt: Der durch den rechten Typ spezifizierte Wert ist als Standardwert für den angegebenen linken Typ gedacht, und die implizite .ne-Steuerung ist dazu da, zu verhindern, dass dieser Wert über die Leitung gesendet wird. Diese Steuerung ist nur sinnvoll, wenn der Steuerungstyp in einem optionalen Kontext verwendet wird; andernfalls gäbe es keine Möglichkeit, den Standardwert zu nutzen.

timer = {
time: uint,
? displayed-step: (number .gt 0) .default 1
}

3.9. Dose/Stecker (Socket/Plug)

Sowohl für Typauswahlen als auch für Gruppenauswahlen ist ein Mechanismus definiert, der es erleichtert, mit leeren Auswahlen zu beginnen und sie später zusammenzusetzen, möglicherweise in separaten Dateien, die verkettet werden, um die vollständige Spezifikation zu erstellen.

Konventionsgemäß werden CDDL-Erweiterungspunkte mit einem führenden Dollarzeichen (Typen) oder zwei führenden Dollarzeichen (Gruppen) markiert. Tools honorieren diese Konvention, indem sie keinen Fehler auslösen, wenn ein solcher Typ oder eine solche Gruppe überhaupt nicht definiert ist; das Symbol wird dann als leere Typauswahl (Gruppenauswahl) angenommen, d. h. es ist keine Auswahl verfügbar.

tcp-header = {seq: uint, ack: uint, * $$tcp-option}

; später, in einer anderen Datei

$$tcp-option //= (
sack: [+(left: uint, right: uint)]
)

; und, vielleicht in einer anderen Datei

$$tcp-option //= (
sack-permitted: true
)

Namen, die mit einem einzelnen "$" beginnen, sind "Typ-Dosen" (Type Sockets), die als leerer Typ beginnen und dazu bestimmt sind, über "/=" erweitert zu werden. Namen, die mit einem doppelten "$$" beginnen, sind "Gruppen-Dosen" (Group Sockets), die als leere Gruppenauswahl beginnen und dazu bestimmt sind, über "//=" erweitert zu werden. In beiden Fällen ist es kein Fehler, wenn es überhaupt keine Definition für eine Dose gibt; dies bedeutet dann, dass es keine Möglichkeit gibt, die Regel zu erfüllen (d. h. die Auswahl ist leer).

Als Konvention müssen alle Definitionen (Stecker) für Dosennamen Augmentationen sein, d. h. sie müssen "/=" bzw. "//=" verwenden.

Um das in Abbildung 7 dargestellte Beispiel aufzugreifen, könnte der Dose/Stecker-Mechanismus wie in Abbildung 12 gezeigt verwendet werden:

PersonalData = {
? displayName: tstr,
NameComponents,
? age: uint,
* $$personaldata-extensions
}

NameComponents = (
? firstName: tstr,
? familyName: tstr,
)

; Das obige funktioniert bereits so wie es ist.
; Aber dann können wir später hinzufügen:

$$personaldata-extensions //= (
favorite-salsa: tstr,
)

; und wieder, irgendwo anders:

$$personaldata-extensions //= (
shoesize: uint,
)

Abbildung 12: Beispiel für persönliche Daten: Verwendung der Dose/Stecker-Erweiterbarkeit

3.10. Generics (Generika)

Unter Verwendung von spitzen Klammern kann die linke Seite einer Regel formale Parameter nach dem zu definierenden Namen hinzufügen, wie in:

messages = message<"reboot", "now"> / message<"sleep", 1..100>
message<t, v> = {type: t, value: v}

Bei Verwendung einer generischen Regel werden die formalen Parameter an die bereitgestellten tatsächlichen Argumente gebunden (ebenfalls unter Verwendung von spitzen Klammern), innerhalb des Bereichs der generischen Regel (als ob es eine Regel der Form parameter = argument gäbe).

Generische Regeln können verwendet werden, um Namen sowohl für Typen als auch für Gruppen festzulegen.

(Zu diesem Zeitpunkt gibt es einige Einschränkungen für die Verschachtelung von Generics in dem in Anhang F beschriebenen CDDL-Tool.)

3.11. Operatorrangfolge

Wie bei jeder Sprache, die mehrere syntaktische Merkmale wie Präfix- und Infix-Operatoren aufweist, hat CDDL Operatoren, die enger binden als andere. Dies wird komplizierter als beispielsweise in ABNF, da CDDL sowohl Typen als auch Gruppen hat, mit Operatoren, die spezifisch für diese Konzepte sind. Typoperatoren (wie "/" für Typauswahl) operieren auf Typen, während Gruppenoperatoren (wie "//" für Gruppenauswahl) auf Gruppen operieren. Typen können einfach in Gruppen verwendet werden, aber Gruppen müssen eingeklammert werden (als Arrays oder Maps), um zu Typen zu werden. Daher binden Typoperatoren natürlich enger als Gruppenoperatoren.

Zum Beispiel ist in

t = [group1]
group1 = (a / b // c / d)
a = 1 b = 2 c = 3 d = 4

group1 eine Gruppenauswahl zwischen der Typauswahl von a und b und der Typauswahl von c und d. Dies wird relevanter, sobald Mitgliederschlüssel und/oder Vorkommen hinzugefügt werden:

t = {group2}
group2 = (? ab: a / b // cd: c / d)
a = 1 b = 2 c = 3 d = 4

ist eine Gruppenauswahl zwischen dem optionalen Mitglied "ab" vom Typ a oder b und dem Mitglied "cd" vom Typ c oder d. Beachten Sie, dass die Optionalität an die erste Auswahl ("ab") gebunden ist, nicht an die zweite Auswahl.

Ebenso ist in

t = [group3]
group3 = (+ a / b / c)
a = 1 b = 2 c = 3

group3 eine Wiederholung einer Typauswahl zwischen a, b und c; wenn nur a wiederholbar sein soll, ist eine Gruppenauswahl erforderlich, um das Vorkommen zu fokussieren:

t = [group4]
group4 = (+ a // b / c)
a = 1 b = 2 c = 3

group4 ist eine Gruppenauswahl zwischen einem wiederholbaren a und einem einzelnen b oder c.

Ein Kommentar war, dass die Semantik von group3 kontraintuitiv sein könnte. Im Allgemeinen wird dem Spezifikationsschreiber wie bei vielen anderen Sprachen mit Operatorrangfolgeregeln empfohlen, sich nicht auf diese zu verlassen, sondern Klammern großzügig einzufügen, um Leser zu leiten, die mit CDDL-Prioritätsregeln nicht vertraut sind:

t = [group4a]
group4a = ((+ a) // (b / c))
a = 1 b = 2 c = 3

Die Operatorrangfolgen sind in der Reihenfolge von lockerer zu enger Bindung in Anhang B definiert und in Tabelle 1 zusammengefasst. (Die angegebenen Stelligkeiten sind 1 für unäre Präfixoperatoren und 2 für binäre Infixoperatoren.)

OperatorStelligkeitOperiert aufRangfolge
=2name = type, name = group1
/=2name /= type1
//=2name //= group1
//2group // group2
,2group, group3
*1* group4
n*m1n*m group4
+1+ group4
?1? group4
=>2type => type5
:2name: type5
/2type / type6
..2type..type7
...2type...type7
.ctrl2type .ctrl type7
&1&group8
~1~type8

Tabelle 1: Zusammenfassung der Operatorrangfolgen