# Protoscope Language Specification. # Protoscope is a text format for representing valid Protobuf wire-format # encodings, directly inspired by https://github.com/google/der-ascii, and # has a significant overlap in syntax with it. # # First, it is reversible, so all encoding variations must be represented in the # language directly. This includes the distinctions between different integer # encodings, packed primitive fields, and groups. # # Second, Protoscope is intended to create both valid and invalid encodings. It # has minimal knowledge of the wire format, but it is ignorant of actual # schemata specified by DescriptorProtos. Elements in the input file may be # freely replaced by raw byte strings, and there is no requirement that the # resulting output is anything resembling a valid proto. # # Protoscope is *not* a replacement for the text format; instead, it is intended # to be used for manipulating encoded protos where the precise encoding is # relevant, such as debugging a codec or creating test data. # # This specification is a valid Protoscope file. # A Protoscope file is a sequence of tokens. Most tokens resolve to a byte # string which is emitted as soon as it is processed. # Tokens are separated by whitespace, which is defined to be space (0x20), TAB # (0x09), CR (0x0d), and LF (0x0a). Apart from acting as a token separator, # whitespace is not significant. # Comments begin with # and run to the end of the line. Comments are treated as # whitespace. # Quoted strings. "Quoted strings are delimited by double quotes. Backslash denotes escape sequences. Legal escape sequences are: \\ \" \x00 \000 \n. \x00 consumes two hex digits and emits a byte. \000 consumes one to three octal digits and emits a byte (rejecting values that do not fit in a single octet). Otherwise, any byte before the closing quote, including a newline, is emitted as-is." # Tokens in the file are emitted one after another, so the following lines # produce the same output: "hello world" "hello " "world" # The Protobuf wire format only deals in UTF-8 when it deals with text at all, # so there is no equivalent of DER-ASCII's UTF-16/32 string literals. # Hex literals. # Backticks denote hex literals. Either uppercase or lowercase is legal, but no # characters other than hexadecimal digits may appear. A hex literal emits the # decoded byte string. `00` `abcdef` `AbCdEf` # Integers. # Tokens which match /-?[0-9]+/ or /-?0x[0-9a-fA-F]+/ are integer tokens. # They encode into a Protobuf varint (base 128). 456 -0xffFF # Signed integers encode as their 64-bit two's complement by default. If an # integer is suffixed with z, it uses the zigzag encoding instead. -2z 3 # Equivalent tokens. # An integer may instead by suffixed with i32 or i64, which indicates it should # be encoded as a fixed-width integer. 0i32 -23i64 # An integer may follow a 'long-form:N' token. This will cause the varint to # have N more bytes than it needs to successfully encode. For example, the # following are equivalent: long-form:3 3 `83808000` # Booleans. # The following tokens emit `01` and `00`, respectively. true false # Floats. # Tokens that match /-?[0-9]+\.[0-9]+([eE]-?[0-9]+)?/ or # /-?0x[0-9a-fA-F]+\.[0-9a-fA-F]+([pP]-?[0-9]+)?/ are floating-point # tokens. They encode to a IEEE 754 binary64 value. 1.0 9.423e-2 -0x1.ffp52 # Decimal floats are only guaranteed a particular encoding when conversion from # decimal to binary is exact. Hex floats always have an exact conversion. The # i32 prefix from above may be used to specify a 32-bit float (i64 is permitted, # but redundant). 1.5i32 0xf.fi64 # The strings inf32, inf64, -inf32, and -inf64 are recognized as shorthands for # 32-bit and 64-bit infinities. There is no shorthand for NaN (since there are # so many of them), and it is best spelled out as a fixed-size hex int. inf32 -inf64 # Tag expressions. # An integer followed by a : denotes a tag expression. This encodes the tag of # a Protobuf field. This is identical to an ordinary integer, except that a # wire type between 0 and 7 is prepended via the expression # # tag := int << 3 | wireType # # The integer specifies the field number, while what follows after the : # specifies the field type. In the examples below, no whitespace may be # present around the : rune. # # Field numbers may be hex, signed, and zigzag per the syntax above, but not # fixed-width. They may have a long-form prefix. 1:VARINT # A varint. 2:I64 # A fixed-width, 64-bit blob. 3:LEN # A length-prefixed blob. 4:SGROUP # A start-group marker. 5:EGROUP # An end-group marker. 6:I32 # A fixed-width, 32-bit blob. 0x10:0 # Also a varint, explicit value for the type. 8:6 # Invalid wire type (6 and 7 are unused). # This is an error: the wire type must be between 0 and 7. # 9:8 # If the : is instead followed by any rune not matching /[\w-]/, the scanner # will seek forward to the next token. If it is a fixed-width integer or a # float, the wire type will be inferred to be I32 or I64 as appropriate; if it # is a {, or a 'long-form:N' followed by a {, the type is inferred as LEN; # if it is a '!', the type is inferred as SGROUP; otherwise, it defaults to # VARINT. 1: 55z 2: 1.23 3: {"text"} 6: -1i32 8: !{42} # Length prefixes. # Matching curly brace tokens denote length prefixes. They emit a varint-encoded # length prefix followed by the encoding of the brace contents. # # It may optionally be preceded by 'long-form:N', as an integer would, to # introduce redundant bytes in the encoding of the length prefix. # This is a string field. Note that 23:'s type is inferred to be LEN. 23: {"my cool string"} # This is a nested message field. 24: { 1: 5 2: {"nested string"} } # This is a packed repeated int field. 25: { 1 2 3 4 5 6 7 } # This string field's length prefix will be 3, rather than one, bytes. 23: long-form:2 {"non-minimally-prefixed"} # Groups # If matching curly braces are prefixed with a ! (no spaces before the first # {), it denotes a group. Encoding a group requires a field number, so the !{} # must come immediately before a tag expression without an explicit type (which # will be inferred to be SGROUP). The closing brace will generate a # corresponding EGROUP-typed tag to match the SGROUP tag. 26: !{ 1: 55z 2: 1.4 3: {"abcd"} } # long-form:N may be the last token between a group's braces, which will be # applied to the EGROUP tag. 27: !{long-form:3} # Examples. # These primitives may be combined with raw byte strings to produce other # encodings. # This is another way to write a message, using an explicit length 2:LEN 4 "abcd" # This allows us to insert the wrong length. 2:LEN 5 "abcd" # The wrong wire type can be used with no consequences. 5:I64 "stuff" # Disassembler. # Although the conversion from Protoscope to a byte string is well-defined, the # inverse is not. A given byte string may have multiple disassemblies. The # disassembler heuristically attempts to give a useful conversion for its # input. # # It is a goal that any valid protobuf input will be decoded reasonably, # although this is impossible in general, because length-prefixed blobs can be # either strings or protobufs, and fixed-width ints can also be floats. We try # to strike a balance that produces mostly readable output. # # Note that the output of the disassembler is not stable, and highly heuristic. # The only guarantee is that it will reassemble to the original input byte for # byte. # # The algorithm is as follows: # # 1. Greedily parse tags out of the input. If an invalid tag is found, encode # the remaining bytes as quoted strings or hex literals. Wire types 6 and 7 # are treated as "invalid". # # 2. Encode the tag as N:, unless the wire type is 4, in which case encode it as # N:EGROUP. However, see below for cases when a wire type 3 tag precedes it. # # 3. If the wire type is 0, parse a varint and encode it as if it were an int64. # There is no useful way to distinguish sint32/sint64 here. # # 4. If the wire type is 1 or 5, parse eight or four bytes and interpret that as # a float of appropriate size. # # a. If the float is a NaN, print the bytes as a fixed-width hex integer. # # b. If the float is infinite, print inf32/inf64 as appropriate. # # c. If the float is zero or has an exponent that is not close to the largest # or smallest possible exponents, print as a decimal float. # # i. If this would produce a non-round-trip-able value, print as a hex # float instead. # # d. Otherwise, print a fixed-width decimal integer. # # 5. If the wire type is 2: # # a. Try to parse the contents of the field as a message, and print those # fields wrapped in {} no failures occur. (-all-fields-are-messages will # instead cause all fields that parsed successfully to be emitted followed # by hex strings with the remaining content.) # # b. Output the contents as quoted strings or hex literals. # # 6. If the wire type is 3: # # a. Encode !{ to begin a group and save the field number on the group stack. # # b. Upon coming to a wire type 4 tag, check if it matches the top of the # group stack. (Pop the stack unconditionally.) # # c. If it does, close the group with a } and do not emit a tag expression. # Emit a long-form:N as necessary. # # d. If it doesn't, re-encode the wire type 3 tag as N:SGROUP, and encode the # wire type 4 tag as above. If a type 2 message ends before the group is # closed, or the input reaches EOF, this step also applies. # # However, Protoscope offers the option of providing a descriptor to aid # disassembly. In this case the heuristic becomes much more intelligent. # Steps 1 and 2 remain the same, but from 3 onwards: # # 3. If the wire type is 0, parse a varint and encode it as: # # a. true/false if the field is bool-typed and 0- or 1-valued. # # b. A sint64 (a 42z literal) if the field is sint32/sint64-typed. # # c. A uint64 if any of the unsigned integer types. # # d. An int64 if none of the above apply. # # 4. If the wire type is 1 or 5, parse eight or four bytes and encode as: # # a. A fixed64/fixed32 (resp) if the field is any of the unsigned integer # types. # # b. A sfixed64/sfixed32 (resp) if the field is any of the other integer # types. # # c. The same way floats are encoded in the schema-less algorithm above, step # 4.a-4.c, but printing subnormals as floats, too. # # 5. If the wire type is 2: # # a. If the field type is a scalar, print as a packed field (numeric literals # inside of braces) per the steps 3 and 4 above. # # b. If the field is a message or group, use the algorithm described in the # schema-less scheme (step 5). # # c. If the field is bytes or string, print as quoted strings or hex # literals as suits the disassembler's fancy. # # 6. If the wire type is 3, proceed per the instructions for a group given for # the schema-less algorithm.