RFC 4506

XDR: External Data Representation Standard

πŸ“… published May 2006, from ietf

XDR: External Data Representation Standard ✨

RFC 4506 πŸŽ€ is an Internet Standards Track doc (STD 67) by M. Eisler, Ed. from Network Appliance, Inc., dated May 2006. It obsoletes RFC 1832, and there's no Updates entry listed. Let's go! πŸ’–

Status of This Memo

This document specifies an Internet standards track protocol for the Internet community, and requests discussion and suggestions for improvements. 🫢 For the standardization state and status of this protocol, check the current edition of the "Internet Official Protocol Standards" (STD 1). Distribution of this memo is unlimited. 🌈

Copyright (C) The Internet Society (2006). πŸ”₯

Abstract

Okay so this doc describes the External Data Representation Standard (XDR) protocol the way it's actually deployed and accepted out in the wild 🌸. It also obsoletes RFC 1832, so that one's officially retired πŸ’….

Table of Contents

1. Introduction ....................................................3
2. Changes from RFC 1832 ...........................................3
3. Basic Block Size ................................................3
4. XDR Data Types ..................................................4
   4.1. Integer ....................................................4
   4.2. Unsigned Integer ...........................................4
   4.3. Enumeration ................................................5
   4.4. Boolean ....................................................5
   4.5. Hyper Integer and Unsigned Hyper Integer ...................5
   4.6. Floating-Point .............................................6
   4.7. Double-Precision Floating-Point ............................7
   4.8. Quadruple-Precision Floating-Point .........................8
   4.9. Fixed-Length Opaque Data ...................................9
   4.10. Variable-Length Opaque Data ...............................9
   4.11. String ...................................................10
   4.12. Fixed-Length Array .......................................11
   4.13. Variable-Length Array ....................................11
   4.14. Structure ................................................12
   4.15. Discriminated Union ......................................12
   4.16. Void .....................................................13
   4.17. Constant .................................................13
   4.18. Typedef ..................................................13
   4.19. Optional-Data ............................................14
   4.20. Areas for Future Enhancement .............................16
5. Discussion .....................................................16
6. The XDR Language Specification .................................17
   6.1. Notational Conventions ....................................17
   6.2. Lexical Notes .............................................18
   6.3. Syntax Information ........................................18
   6.4. Syntax Notes ..............................................20
7. An Example of an XDR Data Description ..........................21
8. Security Considerations ........................................22
9. IANA Considerations ............................................23
10. Trademarks and Owners .........................................23
11. ANSI/IEEE Standard 754-1985 ...................................24
12. Normative References ..........................................25
13. Informative References ........................................25
14. Acknowledgements ..............................................26

1. Introduction 🧚

Okay bestie, XDR is a standard for describing and encoding data πŸ’œ. It's super useful for moving data between totally different computer architectures, and it's been used to talk between machines as diverse as the SUN WORKSTATION*, VAX*, IBM-PC*, and Cray* πŸ–₯️✨. XDR fits into the ISO presentation layer and is roughly analogous in purpose to X.409, ISO Abstract Syntax Notation. The major difference is that XDR uses implicit typing, while X.409 uses explicit typing πŸ™ˆ.

XDR uses a language to describe data formats. Lowkey important: the language can only be used to describe data. It is not a programming language πŸ’…. It lets you describe intricate data formats in a concise way, because the alternative (graphical representations, which are themselves an informal language) gets totally incomprehensible once things get complex 😭. The XDR language itself is similar to the C language [KERN], just as Courier [COUR] is similar to Mesa. Protocols like ONC RPC (Remote Procedure Call) and the NFS* (Network File System) use XDR to describe the format of their data πŸ¦‹.

The XDR standard makes one big assumption: that bytes (or octets) are portable, where a byte is 8 bits of data πŸ“. A given hardware device should encode bytes onto the various media in such a way that other hardware devices can decode them without losing meaning ✨. For example, the Ethernet* standard suggests bytes be encoded in "little-endian" style [COHE], or least significant bit first 🌈.

2. Changes from RFC 1832 πŸŽ€

Here's the tea: this document makes no technical changes to RFC 1832 πŸ™Œ. It's published just to note IANA considerations, augment security considerations, and separate normative references from informative ones πŸ“š.

3. Basic Block Size 🧩

The representation of all items requires a multiple of four bytes (or 32 bits) of data πŸ’–. The bytes are numbered 0 through n-1. They're read or written to some byte stream so that byte m always comes before byte m+1 🫢. If the n bytes needed to hold the data aren't a multiple of four, then the n bytes get followed by enough (0 to 3) residual zero bytes, r, to make the total byte count a multiple of 4 🍭.

We include the familiar graphic box notation for illustration and comparison πŸ¦‹. In most illustrations, each box (delimited by a plus sign at the 4 corners and vertical bars and dashes) depicts a byte πŸ’….

Ellipses (...) between boxes show zero or more additional bytes where required ✨.

     +--------+--------+...+--------+--------+...+--------+
     | byte 0 | byte 1 |...|byte n-1|    0   |...|    0   |   BLOCK
     +--------+--------+...+--------+--------+...+--------+
     |<-----------n bytes---------->|<------r bytes------>|
     |<-----------n+r (where (n+r) mod 4 = 0)>----------->|

4. XDR Data Types 🌸

Each section that follows describes a data type defined in the XDR standard, shows how it's declared in the language, and includes a graphic illustration of its encoding πŸŽ€.

For each data type in the language we show a general paradigm declaration πŸ’œ. Note that angle brackets (< and >) denote variable-length sequences of data, and square brackets ([ and ]) denote fixed-length sequences of data πŸ“. n, m, and r denote integers πŸ”’. For the full language specification and more formal definitions of terms like "identifier" and "declaration", refer to Section 6, "The XDR Language Specification" πŸ“–.

For some data types, more specific examples are included. A more extensive example of a data description is in Section 7, "An Example of an XDR Data Description" 🧁.

4.1. Integer πŸ’–

An XDR signed integer is a 32-bit datum that encodes an integer in the range [-2147483648,2147483647]. The integer is represented in two's complement notation. The most and least significant bytes are 0 and 3, respectively. Integers are declared like this 🫢:

      int identifier;

        (MSB)                   (LSB)
      +-------+-------+-------+-------+
      |byte 0 |byte 1 |byte 2 |byte 3 |                      INTEGER
      +-------+-------+-------+-------+
      <------------32 bits------------>

4.2. Unsigned Integer ✨

An XDR unsigned integer is a 32-bit datum that encodes a non-negative integer in the range [0,4294967295] 🌈. It's represented by an unsigned binary number whose most and least significant bytes are 0 and 3, respectively. An unsigned integer is declared like this πŸ’…:

      unsigned int identifier;

        (MSB)                   (LSB)
         +-------+-------+-------+-------+
         |byte 0 |byte 1 |byte 2 |byte 3 |           UNSIGNED INTEGER
         +-------+-------+-------+-------+
         <------------32 bits------------>

4.3. Enumeration πŸ¦‹

Enumerations have the same representation as signed integers πŸ’œ. They're handy for describing subsets of the integers πŸ“. Enumerated data is declared like this:

      enum { name-identifier = constant, ... } identifier;

For example, the three colors red, yellow, and blue could be described by an enumerated type 🎨:

      enum { RED = 2, YELLOW = 3, BLUE = 5 } colors;

It is an error to encode as an enum any integer other than those that have been given assignments in the enum declaration πŸ™…β€β™€οΈ.

4.4. Boolean 🌟

Booleans are important enough, and show up often enough, to get their own explicit type in the standard 🫢. They're declared like this:

      bool identifier;

This is equivalent to:

      enum { FALSE = 0, TRUE = 1 } identifier;

4.5. Hyper Integer and Unsigned Hyper Integer 🧚

The standard also defines 64-bit (8-byte) numbers called hyper integers and unsigned hyper integers πŸ’–. Their representations are the obvious extensions of integer and unsigned integer, defined above ✨. They're represented in two's complement notation. The most and least significant bytes are 0 and 7, respectively. Their declarations:

   hyper identifier; unsigned hyper identifier;

        (MSB)                                                   (LSB)
      +-------+-------+-------+-------+-------+-------+-------+-------+
      |byte 0 |byte 1 |byte 2 |byte 3 |byte 4 |byte 5 |byte 6 |byte 7 |
      +-------+-------+-------+-------+-------+-------+-------+-------+
      <----------------------------64 bits---------------------------->
                                                 HYPER INTEGER
                                                 UNSIGNED HYPER INTEGER

to be continued in part 2 πŸ’œ

4.6. Floating-Point πŸ¦‹

okay so the standard gives us the float type, and it's 32 bits or 4 bytes πŸ’– The encoding is the IEEE standard for normalized single-precision floating-point numbers [IEEE]. Three fields describe the number:

  • S: the sign. 0 means positive and 1 means negative. One bit 🌸
  • E: the exponent, base 2. This one gets 8 bits, and it's biased by 127 ✨
  • F: the fractional part of the mantissa, base 2. This gets 23 bits πŸŽ€

So the number works out to:

(-1)**S * 2**(E-Bias) * 1.F

and you declare it like this:

float identifier;

Here's the layout, bestie πŸ‘‘

+-------+-------+-------+-------+
|byte 0 |byte 1 |byte 2 |byte 3 |              SINGLE-PRECISION
S|   E   |           F          |         FLOATING-POINT NUMBER
+-------+-------+-------+-------+
1|<- 8 ->|<-------23 bits------>|
<------------32 bits------------>

Just like the most and least significant bytes are 0 and 3, the most and least significant bits of a single-precision float are 0 and 31. The beginning bit offsets (and most significant bit offsets) for S, E, and F are 0, 1, and 9. Just know these numbers are the mathematical positions of the bits, and they are not their actual physical locations, which change from medium to medium πŸ™ˆ

For signed zero, signed infinity (overflow), and denormalized numbers (underflow), go check the IEEE specs [IEEE]. According to IEEE, "NaN" (not a number) is system dependent, and within XDR it should not be read as anything other than "NaN" πŸ’€


4.7. Double-Precision Floating-Point 🌈

the double type is 64 bits or 8 bytes, and it's encoded with the IEEE standard for normalized double-precision floating-point numbers [IEEE]. It has the same three fields, just bigger:

  • S: the sign. 0 is positive, 1 is negative. One bit πŸ“
  • E: the exponent, base 2. 11 bits go here, biased by 1023 🧚
  • F: the fractional part of the mantissa, base 2. 52 bits go here πŸ’œ

The math is the same:

(-1)**S * 2**(E-Bias) * 1.F

Declare it with:

double identifier;

And the layout is:

+------+------+------+------+------+------+------+------+
|byte 0|byte 1|byte 2|byte 3|byte 4|byte 5|byte 6|byte 7|
S|    E   |                    F                        |
+------+------+------+------+------+------+------+------+
1|<--11-->|<-----------------52 bits------------------->|
<-----------------------64 bits------------------------->
                                DOUBLE-PRECISION FLOATING-POINT

Same deal with the bits: the most and least significant bits are 0 and 63, and the beginning bit offsets for S, E, and F are 0, 1, and 12. Again, those are mathematical positions, not where the bits physically live πŸ¦‹

The rules for signed zero, signed infinity (overflow), and denormalized numbers (underflow) are in the IEEE specs [IEEE]. And "NaN" is system dependent here too, so within XDR it should not be treated as anything other than "NaN" ✨


4.8. Quadruple-Precision Floating-Point πŸ”₯

the quadruple type is 128 bits or 16 bytes. The encoding is designed to be a simple analog of the single- and double-precision encodings, using one form of IEEE double extended precision. Its fields are:

  • S: the sign. 0 is positive and 1 is negative. One bit πŸŽ€
  • E: the exponent, base 2. 15 bits, biased by 16383 πŸ‘‘
  • F: the fractional part of the mantissa, base 2. 112 bits 🌸

The formula stays the same:

(-1)**S * 2**(E-Bias) * 1.F

Declaration:

quadruple identifier;

Layout:

+------+------+------+------+------+------+-...--+------+
|byte 0|byte 1|byte 2|byte 3|byte 4|byte 5| ...  |byte15|
S|    E       |                  F                      |
+------+------+------+------+------+------+-...--+------+
1|<----15---->|<-------------112 bits------------------>|
<-----------------------128 bits------------------------>
                              QUADRUPLE-PRECISION FLOATING-POINT

The most and least significant bits are 0 and 127. The beginning bit offsets for S, E, and F are 0, 1, and 16. These are mathematical positions, not physical locations, which can vary from medium to medium 🧚

For signed zero, signed infinity (overflow), and denormalized numbers, the encodings are analogs of the single- and double-precision ones [SPAR], [HPRE]. And the "NaN" encoding for quadruple-precision numbers is system dependent too, so within XDR it should not be interpreted as anything other than "NaN" πŸ’…


4.9. Fixed-Length Opaque Data 🫢

sometimes fixed-length data that nobody interprets needs to get passed between machines. That's called "opaque" data, and you declare it like this:

opaque identifier[n];

Here n is the (static) number of bytes needed to hold the opaque data. If n isn't a multiple of four, the n bytes get followed by enough residual zero bytes, r (anywhere from 0 to 3), so the total byte count is a multiple of four πŸ“

 0        1     ...
+--------+--------+...+--------+--------+...+--------+
| byte 0 | byte 1 |...|byte n-1|    0   |...|    0   |
+--------+--------+...+--------+--------+...+--------+
|<-----------n bytes---------->|<------r bytes------>|
|<-----------n+r (where (n+r) mod 4 = 0)------------>|
                                             FIXED-LENGTH OPAQUE

4.10. Variable-Length Opaque Data πŸ’œ

this one's the counted version. It's a sequence of n arbitrary bytes, numbered 0 through n-1. The number n gets encoded as an unsigned integer (like we covered above), and then the n bytes follow it 🌈

Byte m of the sequence always comes before byte m+1, and byte 0 always comes right after the length (count). Same padding rule as before: if n isn't a multiple of four, you add 0 to 3 residual zero bytes, r, so the total is a multiple of four ✨

You can declare it in either of these ways:

opaque identifier<m>;

or

opaque identifier<>;

The constant m is an upper bound on how many bytes the sequence can hold. If you skip m like in the second one, it's assumed to be (2**32) - 1, the maximum length πŸ¦‹

The constant m usually comes from a protocol spec. For example, a filing protocol might say the max data transfer size is 8192 bytes, so you'd write:

opaque filedata<8192>;
         0     1     2     3     4     5   ...
      +-----+-----+-----+-----+-----+-----+...+-----+-----+...+-----+
      |        length n       |byte0|byte1|...| n-1 |  0  |...|  0  |
      +-----+-----+-----+-----+-----+-----+...+-----+-----+...+-----+
      |<-------4 bytes------->|<------n bytes------>|<---r bytes--->|
                              |<----n+r (where (n+r) mod 4 = 0)---->|
                                               VARIABLE-LENGTH OPAQUE

And if you encode a length that's bigger than the max in the spec, that's an error πŸ™ˆ


4.11. String πŸŽ€

a string is basically the same deal. It's a string of n ASCII bytes, numbered 0 through n-1. The number n gets encoded as an unsigned integer (described above), and then the n bytes follow it πŸ’–

Byte m of the string always comes before byte m+1, and byte 0 always follows the string's length. If n isn't a multiple of four, you add 0 to 3 residual zero bytes, r, to make the total byte count a multiple of four πŸ“

Counted strings get declared like this:

string object<m>;

or

string object<>;

Same rules as before: m is an upper bound on the bytes the string can contain. Skip it, like in the second form, and it's assumed to be (2**32) - 1, the maximum length. The constant m would normally come from a protocol spec. Say a filing protocol says file names can be no longer than 255 bytes. You'd write:

string filename<255>;
         0     1     2     3     4     5   ...
      +-----+-----+-----+-----+-----+-----+...+-----+-----+...+-----+
      |        length n       |byte0|byte1|...| n-1 |  0  |...|  0  |
      +-----+-----+-----+-----+-----+-----+...+-----+-----+...+-----+
      |<-------4 bytes------->|<------n bytes------>|<---r bytes--->|
                              |<----n+r (where (n+r) mod 4 = 0)---->|
                                                              STRING

And encoding a length above the max in the spec is an error, same as before πŸ’…


4.12. Fixed-Length Array 🧚

fixed-length arrays of homogeneous elements are declared like this:

type-name identifier[n];

The elements, numbered 0 through n-1, get encoded one at a time in their natural order, 0 through n-1. Each element's size is a multiple of four bytes ✨

Here's the twist: all the elements are the same type, but they don't all have to be the same size. Like, in a fixed-length array of strings, every element is type "string", but each one can have a different length 🌸

+---+---+---+---+---+---+---+---+...+---+---+---+---+
|   element 0   |   element 1   |...|  element n-1  |
+---+---+---+---+---+---+---+---+...+---+---+---+---+
|<--------------------n elements------------------->|

                                      FIXED-LENGTH ARRAY

4.13. Variable-Length Array ✨

Okay bestie, counted arrays are how you send a list of same-type things when you don't know how many there'll be πŸ¦‹. The encoding starts with the element count n (an unsigned integer), then each element in order, starting at element 0 and going all the way to element n-1. The declaration comes in two flavours:

type-name identifier<m>;
   or
type-name identifier<>;

The constant m is the max element count you'll accept for the array. If you leave it out like in the second line, it's assumed to be (2**32) - 1, which is a lot of elements πŸ’…

        0  1  2  3
      +--+--+--+--+--+--+--+--+--+--+--+--+...+--+--+--+--+
      |     n     | element 0 | element 1 |...|element n-1|
      +--+--+--+--+--+--+--+--+--+--+--+--+...+--+--+--+--+
      |<-4 bytes->|<--------------n elements------------->|
                                                      COUNTED ARRAY

It's an error to encode an n that goes over the maximum from the spec, so don't even try πŸ™…β€β™€οΈ

4.14. Structure πŸŽ€

Structures are declared like this:

struct {
   component-declaration-A;
   component-declaration-B;
   ...
} identifier;

The components get encoded in the same order they're declared in the structure. Each component's size is a multiple of four bytes, but the components can be different sizes from each other, which is giving flexibility 🌸

+-------------+-------------+...
| component A | component B |...                      STRUCTURE
+-------------+-------------+...

4.15. Discriminated Union πŸ¦‹

A discriminated union is a type made of a discriminant followed by a type picked from a set of prearranged types, depending on what the discriminant's value is. The discriminant has to be either "int", "unsigned int", or an enumerated type like "bool". The possible types are called "arms" of the union, and each arm is preceded by the discriminant value that tells you how to encode it. Here's the declaration:

union switch (discriminant-declaration) {
case discriminant-value-A:
   arm-declaration-A;
case discriminant-value-B:
   arm-declaration-B;
...
default: default-declaration;
} identifier;

Every case keyword gets followed by a legal discriminant value. The default arm is optional 🫢 If you skip it, then a valid encoding of the union can't have discriminant values that weren't specified. Also, the size of the arm you end up with is always a multiple of four bytes.

The union gets encoded as the discriminant, then the encoding of the arm it implies:

 0   1   2   3
+---+---+---+---+---+---+---+---+
|  discriminant |  implied arm  |          DISCRIMINATED UNION
+---+---+---+---+---+---+---+---+
|<---4 bytes--->|

4.16. Void 🌈

An XDR void is a 0-byte quantity, so it's literally nothing but iconic πŸ’€ Voids are useful for operations that take no data as input, or no data as output. They also come in handy inside unions, where some arms have data and others don't. The declaration is just:

void;

And here's what a void looks like:

  ++
  ||                                                     VOID
  ++
--><-- 0 bytes

4.17. Constant πŸ’œ

The declaration for a constant looks like this:

const name-identifier = n;

const gives a symbolic name to a constant, but it doesn't declare any data, so it's not a variable 🧚 You can use the symbolic constant anywhere a regular constant would work. For example, this one defines a symbolic constant called DOZEN, which equals 12:

const DOZEN = 12;

4.18. Typedef πŸ“

typedef doesn't declare any data either. It just defines new identifiers you can use when declaring data. The syntax is:

typedef declaration;

The new type name is actually the variable name sitting in the declaration part of the typedef. For example, this defines a new type called eggbox, built from an existing type called egg:

typedef egg eggbox[DOZEN];

A variable declared with the new type name has the same type that the new name would have if you treated it as a variable. So these two declarations are equivalent when it comes to declaring the variable fresheggs:

eggbox  fresheggs;
egg     fresheggs[DOZEN];

When a typedef involves a struct, enum, or union definition, there's another syntax that's preferred for defining the same type. Generally, a typedef like this:

typedef <<struct, union, or enum definition>> identifier;

can be rewritten by dropping the typedef part and putting the identifier right after the struct, union, or enum keyword, instead of at the end. For example, here are the two ways of defining the bool type:

typedef enum {    /* using typedef */
   FALSE = 0,
   TRUE = 1
} bool;

enum bool {       /* preferred alternative */
   FALSE = 0,
   TRUE = 1
};

The preferred version is nicer because you don't have to wait until the very end of a declaration to find out what the new type is called ✨

4.19. Optional-Data 🌸

Optional-data is a kind of union that shows up so often that it gets its own special syntax. It's declared like this:

type-name *identifier;

That's equivalent to this union:

union switch (bool opted) {
case TRUE:
   type-name element;
case FALSE:
   void;
} identifier;

It's also equivalent to this variable-length array declaration, since the boolean opted can be read as the array's length:

type-name identifier<1>;

On its own, optional-data isn't super exciting, but it's really useful for recursive data structures like linked lists and trees 🌳 For example, this defines a type stringlist that encodes lists of zero or more arbitrary-length strings:

struct stringentry {
   string item<>;
   stringentry *next;
};

typedef stringentry *stringlist;

It could have been declared the same way as this union:

union stringlist switch (bool opted) {
case TRUE:
   struct {
      string item<>;
      stringlist next;
   } element;
case FALSE:
   void;
};

or as a variable-length array:

struct stringentry {
   string item<>;
   stringentry next<1>;
};

typedef stringentry stringlist<1>;

Both of those declarations hide what the stringlist type is really for, so the optional-data declaration is the one that's preferred over both of them πŸ™ˆ The optional-data type also lines up nicely with how recursive data structures get represented in high-level languages like Pascal or C using pointers. Honestly, the syntax is the same as C's syntax for pointers, which is iconic πŸ‘‘

4.20. Areas for Future Enhancement πŸš€

XDR is missing representations for bit fields and bitmaps, since the standard is based on bytes. Packed (or binary-coded) decimals aren't in there either πŸ€·β€β™€οΈ

The point of XDR was never to describe every kind of data people have ever sent or will ever want to send between machines. It only covers the most commonly used data types of high-level languages like Pascal or C, so that apps written in those languages can talk to each other easily over some medium πŸ’–

You could imagine extending XDR so it could describe almost any existing protocol, like TCP. The minimum you'd need for that is support for different block sizes and byte-orders. The XDR we're talking about here could then be thought of as the 4-byte big-endian member of a bigger XDR family πŸ¦‹

5. Discussion πŸ’¬

(1) Why use a language for describing data? What's wrong with diagrams?

There are lots of advantages to using a data-description language like XDR instead of diagrams πŸ’… Languages are more formal than diagrams, so they lead to less ambiguous descriptions of data. They're also easier to understand, and they let you think about other issues instead of getting lost in low-level bit encoding details 🧠 There's also a close analogy between XDR's types and a high-level language like C or Pascal, which makes implementing XDR encoding and decoding modules way easier. And finally, the language specification is itself an ASCII string, so it can be passed from machine to machine to do on-the-fly data interpretation ✨

(2) Why is there only one byte-order for an XDR unit?

Supporting two byte-orderings would require a higher-level protocol to figure out which byte-order the data is encoded in. Since XDR isn't a protocol, that can't happen πŸ™ƒ The upside though? Data in XDR format can be written to a magnetic tape, for example, and any machine will be able to read it, since no higher-level protocol is needed to figure out the byte-order 🧲

(3) Why is the XDR byte-order big-endian instead of little-endian? Isn't this unfair to little-endian machines such as the VAX(r), which has to convert from one form to the other?

Yes, it's unfair, but having only one byte-order means you have to be unfair to somebody 😭 Lots of architectures, like the Motorola 68000* and IBM 370*, support the big-endian byte-order.

(4) Why is the XDR unit four bytes wide?

It's a tradeoff πŸ₯Ή Picking a small size, like two, keeps the encoded data small, but causes alignment problems for machines that aren't aligned on those boundaries. A large size, like eight, means the data will be aligned on virtually every machine, but makes the encoded data grow too big. So we went with four as a compromise. Four is big enough to support most architectures efficiently, except for rare machines like the eight-byte-aligned Cray*. And four is small enough to keep the encoded data at a reasonable size πŸ’–

(5) Why must variable-length data be padded with zeros?

It's important that the same data encodes into the same thing on all machines, so encoded data can be meaningfully compared or checksummed πŸ” Forcing the padded bytes to be zero is what makes that happen 🫢

(6) Why is there no explicit data-typing?

Data-typing costs a lot for the small advantages it might give 😀 One cost is the expansion of data from the inserted type fields. Another is the extra work of interpreting those type fields and acting on them. And most protocols already know what type they expect, so data-typing only supplies redundant information. That said, you can still get the benefits of data-typing with XDR 🌈 One way is to encode two things: first a string that's the XDR data description of the encoded data, and then the encoded data itself. Another way is to assign a value to all the types in XDR, then define a universal type that takes this value as its discriminant and, for each value, describes the corresponding data type 🧚

6. The XDR Language Specification ✨

6.1. Notational Conventions

Okay so XDR's language gets described with an extended Backus-Naur Form notation 🧠✨ Here's the quick rundown:

  1. The characters |, (, ), [, ], ", and * are special. πŸŽ€
  2. Terminal symbols are strings of any characters surrounded by double quotes. πŸ’–
  3. Non-terminal symbols are strings of non-special characters. 🌸
  4. Alternative items are separated by a vertical bar (|). πŸ¦‹
  5. Optional items are enclosed in brackets. πŸ“
  6. Items are grouped together by enclosing them in parentheses. 🧚
  7. A * following an item means 0 or more occurrences of that item. πŸ’…

Here's an example pattern to make it click:

"a " "very" (", " "very")* [" cold " "and "]  " rainy "
("day" | "night")

An infinite number of strings match this pattern, and a few of them are:

"a very rainy day"
"a very, very rainy day"
"a very cold and  rainy day"
"a very, very, very cold and  rainy night"

6.2. Lexical Notes

Lexical notes, bestie, let's gooo πŸ”₯

  1. Comments begin with /* and terminate with */. πŸ’œ
  2. White space serves to separate items and is otherwise ignored. 🫢
  3. An identifier is a letter followed by an optional sequence of letters, digits, or underbar (_). The case of identifiers is not ignored, so capital letters matter! πŸ’…
  4. A decimal constant expresses a number in base 10 and is a sequence of one or more decimal digits, where the first digit is not a zero, and is optionally preceded by a minus-sign (-). ✨
  5. A hexadecimal constant expresses a number in base 16, and must be preceded by 0x, followed by one or hexadecimal digits ('A', 'B', 'C', 'D', E', 'F', 'a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'). 🌈
  6. An octal constant expresses a number in base 8, always leads with digit 0, and is a sequence of one or more octal digits ('0', '1', '2', '3', '4', '5', '6', '7'). πŸ“

6.3. Syntax Information

Here's the whole grammar in one place, and honestly it's giving very organised πŸ’–

   declaration:
        type-specifier identifier
      | type-specifier identifier "[" value "]"
      | type-specifier identifier "<" [ value ] ">"
      | "opaque" identifier "[" value "]"
      | "opaque" identifier "<" [ value ] ">"
      | "string" identifier "<" [ value ] ">"
      | type-specifier "*" identifier
      | "void"

   value:
        constant
      | identifier

   constant:
      decimal-constant | hexadecimal-constant | octal-constant

   type-specifier:
        [ "unsigned" ] "int"
      | [ "unsigned" ] "hyper"
      | "float"
      | "double"
      | "quadruple"
      | "bool"
      | enum-type-spec
      | struct-type-spec
      | union-type-spec
      | identifier

   enum-type-spec:
      "enum" enum-body

   enum-body:
      "{"
         ( identifier "=" value )
         ( "," identifier "=" value )*
      "}"

   struct-type-spec:
      "struct" struct-body

   struct-body:
      "{"
         ( declaration ";" )
         ( declaration ";" )*
      "}"

   union-type-spec:
      "union" union-body

   union-body:
      "switch" "(" declaration ")" "{"
         case-spec
         case-spec *
         [ "default" ":" declaration ";" ]
      "}"

   case-spec:
     ( "case" value ":")
     ( "case" value ":") *
     declaration ";"

   constant-def:
      "const" identifier "=" constant ";"

   type-def:
        "typedef" declaration ";"
      | "enum" identifier enum-body ";"
      | "struct" identifier struct-body ";"
      | "union" identifier union-body ";"

   definition:
        type-def
      | constant-def

   specification:
        definition *

6.4. Syntax Notes

Now the rules that the grammar can't express on its own. These are the fine print, and they matter πŸ‘€

  1. The following are keywords and cannot be used as identifiers: "bool", "case", "const", "default", "double", "quadruple", "enum", "float", "hyper", "int", "opaque", "string", "struct", "switch", "typedef", "union", "unsigned", and "void". 🚫
  2. Only unsigned constants may be used as size specifications for arrays. If an identifier is used, it must have been declared previously as an unsigned constant in a "const" definition. πŸŽ€
  3. Constant and type identifiers within the scope of a specification are in the same name space and must be declared uniquely within this scope. πŸ’…
  4. Similarly, variable names must be unique within the scope of struct and union declarations. Nested struct and union declarations create new scopes. 🧚
  5. The discriminant of a union must be of a type that evaluates to an integer. That is, "int", "unsigned int", "bool", an enumerated type, or any typedefed type that evaluates to one of these is legal. Also, the case values must be one of the legal values of the discriminant. Finally, a case value may not be specified more than once within the scope of a union declaration. πŸ¦‹

7. An Example of an XDR Data Description

Time for the fun part πŸŽ€ Here's a short XDR description of a thing called a "file," the kind of thing you'd use to send files from one machine to another.

      const MAXUSERNAME = 32;     /* max length of a user name */
      const MAXFILELEN = 65535;   /* max length of a file      */
      const MAXNAMELEN = 255;     /* max length of a file name */

      /*
       * Types of files:
       */
      enum filekind {
         TEXT = 0,       /* ascii data */
         DATA = 1,       /* raw data   */
         EXEC = 2        /* executable */
      };

      /*
       * File information, per kind of file:
       */
      union filetype switch (filekind kind) {
      case TEXT:
         void;                           /* no extra information */
      case DATA:
         string creator<MAXNAMELEN>;     /* data creator         */
      case EXEC:
         string interpretor<MAXNAMELEN>; /* program interpretor  */
      };

      /*
       * A complete file:
       */
      struct file {
         string filename<MAXNAMELEN>; /* name of file    */
         filetype type;               /* info about file */
         string owner<MAXUSERNAME>;   /* owner of file   */
         opaque data<MAXFILELEN>;     /* file data       */
      };

Okay so picture a user named "john" who wants to store his lisp program "sillyprog," which contains just the data "(quit)". 🧚 Here's how his file gets encoded, byte by byte:

    OFFSET  HEX BYTES       ASCII    COMMENTS
    ------  ---------       -----    --------
     0      00 00 00 09     ....     -- length of filename = 9
     4      73 69 6c 6c     sill     -- filename characters
     8      79 70 72 6f     ypro     -- ... and more characters ...
    12      67 00 00 00     g...     -- ... and 3 zero-bytes of fill
    16      00 00 00 02     ....     -- filekind is EXEC = 2
    20      00 00 00 04     ....     -- length of interpretor = 4
    24      6c 69 73 70     lisp     -- interpretor characters
    28      00 00 00 04     ....     -- length of owner = 4
    32      6a 6f 68 6e     john     -- owner characters
    36      00 00 00 06     ....     -- length of file data = 6
    40      28 71 75 69     (qui     -- file data bytes ...
    44      74 29 00 00     t)..     -- ... and 2 zero-bytes of fill

8. Security Considerations

Serious talk time, bestie 🫢 XDR is a data description language, not a protocol. So it doesn't carry its own particular security considerations by itself. Protocols that carry XDR-formatted data, like NFSv4, are responsible for providing whatever security services are needed to protect the data they transport πŸ”

Still, encoding and decoding have to be done properly to avoid attacks 😭 Here are the known and avoidable risks:

  • Buffer overflow attacks. Where feasible, protocols should be defined with explicit limits (via the "<" [ value ] ">" notation instead of "<" ">") on elements with variable-length data types. Regardless of whether an explicit limit is feasible for a given protocol, decoders need to ensure the incoming size does not exceed the length of any provisioned receiver buffers. πŸ’€
  • Nul octets embedded in an encoded value of type string. If the decoder's native string format uses nul-terminated strings, then the apparent size of the decoded object will be less than the amount of memory allocated for the string. Some memory deallocation interfaces take a size argument. The caller of the deallocation interface would likely determine the size of the string by counting to the location of the nul octet and adding one. This discrepancy can cause memory leakage (because less memory is actually returned to the free pool than allocated), leading to system failure and a denial of service attack. πŸ™ˆ
  • Decoding of characters in strings that are legal ASCII characters but nonetheless are illegal for the intended application. For example, some operating systems treat the / character as a component separator in path names. For a protocol that encodes a string in the argument to a file creation operation, the decoder needs to ensure that / is not inside the component name. Otherwise, a file with an illegal / in its name will be created, making it difficult to remove, and is therefore a denial of service attack. πŸ”₯
  • Denial of service caused by recursive decoder or encoder subroutines. A recursive decoder or encoder might process data that has a structured type with a member of type optional data that directly or indirectly refers to the structured type (i.e., a linked list). For example:
      struct m {
        int x;
        struct m *next;
      };

An encoder or decoder subroutine might be written to recursively call itself each time another element of type "struct m" is found. An attacker could construct a long linked list of "struct m" elements in the request or response, which then causes a stack overflow on the decoder or encoder. Decoders and encoders should be written non-recursively or impose a limit on list length. πŸ‘‘

9. IANA Considerations

It's possible, if not likely, that new data types will be added to XDR in the future. ✨ New types get added through a standards track RFC, not by registering them with IANA. Standards track RFCs that update or replace this document should be documented as such in the RFC Editor's database of RFCs.

10. Trademarks and Owners

Last but not least, the trademark credits πŸ’…

   SUN WORKSTATION  Sun Microsystems, Inc.
   VAX              Hewlett-Packard Company
   IBM-PC           International Business Machines Corporation
   Cray             Cray Inc.
   NFS              Sun Microsystems, Inc.
   Ethernet         Xerox Corporation.
   Motorola 68000   Motorola, Inc.
   IBM 370          International Business Machines Corporation

11. ANSI/IEEE Standard 754-1985 ✨

okay so this section is where the floating point stuff lives 🧚 the definitions of NaNs, signed zero, infinity, and denormalized numbers are copied over from [IEEE] just so you have them handy. bestie, the quad-precision ones are basically the same vibe as single and double, just bigger πŸ’…

we're using 'S' for the sign bit, 'E' for the exponent, and 'F' for the fractional part. the 'u' means an undefined bit, so it can be a 0 or a 1 and we don't care πŸ€·β€β™€οΈ

For single-precision floating point numbers:

 Type                  S (1 bit)   E (8 bits)    F (23 bits)
 ----                  ---------   ----------    -----------
 signalling NaN        u           255 (max)     .0uuuuu---u
                                                 (with at least
                                                  one 1 bit)
 quiet NaN             u           255 (max)     .1uuuuu---u

 negative infinity     1           255 (max)     .000000---0

 positive infinity     0           255 (max)     .000000---0

 negative zero         1           0             .000000---0

 positive zero         0           0             .000000---0

For double-precision floating point numbers:

 Type                  S (1 bit)   E (11 bits)   F (52 bits)
 ----                  ---------   -----------   -----------
 signalling NaN        u           2047 (max)    .0uuuuu---u
                                                 (with at least
                                                  one 1 bit)
 quiet NaN             u           2047 (max)    .1uuuuu---u

 negative infinity     1           2047 (max)    .000000---0

 positive infinity     0           2047 (max)    .000000---0

 negative zero         1           0             .000000---0

 positive zero         0           0             .000000---0

For quadruple-precision floating point numbers:

 Type                  S (1 bit)   E (15 bits)   F (112 bits)
 ----                  ---------   -----------   ------------
 signalling NaN        u           32767 (max)   .0uuuuu---u
                                                 (with at least
                                                  one 1 bit)
 quiet NaN             u           32767 (max)   .1uuuuu---u

 negative infinity     1           32767 (max)   .000000---0

 positive infinity     0           32767 (max)   .000000---0

 negative zero         1           0             .000000---0

 positive zero         0           0             .000000---0

Subnormal numbers are represented as follows:

 Precision            Exponent       Value
 ---------            --------       -----
 Single               0              (-1)**S * 2**(-126) * 0.F

 Double               0              (-1)**S * 2**(-1022) * 0.F

 Quadruple            0              (-1)**S * 2**(-16382) * 0.F

12. Normative References πŸ“š

okay the normative stuff is short and sweet πŸŽ€ this is the one reference you actually need to follow:

[IEEE] "IEEE Standard for Binary Floating-Point Arithmetic", ANSI/IEEE Standard 754-1985, Institute of Electrical and Electronics Engineers, August 1985.

13. Informative References 🌸

these ones are the extra reading, nice to know but not required πŸ€“

[KERN] Brian W. Kernighan & Dennis M. Ritchie, "The C Programming Language", Bell Laboratories, Murray Hill, New Jersey, 1978.

[COHE] Danny Cohen, "On Holy Wars and a Plea for Peace", IEEE Computer, October 1981.

[COUR] "Courier: The Remote Procedure Call Protocol", XEROX Corporation, XSIS 038112, December 1981.

[SPAR] "The SPARC Architecture Manual: Version 8", Prentice Hall, ISBN 0-13-825001-4.

[HPRE] "HP Precision Architecture Handbook", June 1987, 5954-9906.

14. Acknowledgements 🫢

the people who made this possible, we love them πŸ’–

Bob Lyon was Sun's visible force behind ONC RPC in the 1980s. Sun Microsystems, Inc., is listed as the author of RFC 1014. Raj Srinivasan and the rest of the old ONC RPC working group edited RFC 1014 into RFC 1832, from which this document is derived. Mike Eisler and Bill Janssen submitted the implementation reports for this standard. Kevin Coffman, Benny Halevy, and Jon Peterson reviewed this document and gave feedback. Peter Astrand and Bryan Olson pointed out several errors in RFC 1832 which are corrected in this document.

Editor's Address πŸ“¬

Mike Eisler 5765 Chase Point Circle Colorado Springs, CO 80919 USA

Phone: 719-599-9026 EMail: email2mre-rfc4506@yahoo.com

Please address comments to: nfsv4@ietf.org

Copyright (C) The Internet Society (2006).

ngl the legal part is kind of a mood, but here's the deal: this document is subject to the rights, licenses and restrictions in BCP 78, and except as set forth there, the authors keep all their rights.

The document and the information in it are provided on an "AS IS" basis. THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.

Intellectual Property πŸ”

The IETF doesn't take a side on whether any Intellectual Property Rights or other rights that might be claimed to apply to the technology in this document are valid or how far they reach. It also doesn't say whether a license for those rights is available, and it hasn't done any independent digging to find out what rights exist. You can find the procedures for rights in RFC documents in BCP 78 and BCP 79.

Copies of IPR disclosures made to the IETF Secretariat, along with any assurances of licenses that might be offered, or the results of an attempt to get a general license or permission to use those proprietary rights for implementers or users of this specification, can be found in the IETF on-line IPR repository at http://www.ietf.org/ipr.

The IETF invites anyone interested to tell it about any copyrights, patents, patent applications, or other proprietary rights that may cover technology needed to implement this standard. Send that information to the IETF at ietf-ipr@ietf.org.

Acknowledgement πŸ’œ

Funding for the RFC Editor function is provided by the IETF Administrative Support Activity (IASA).

πŸ¦‹ and that's a wrap on RFC 4506, bestie. you made it through the whole thing πŸ‘‘

πŸ“¦ where this page comes from