‹ All Air Kalon docs

Air Kalon

Wire format

Byte-level reference for the mode 2 frame, the acknowledgement frame and the KCF1 file container.

Conventions

  • All multi-byte integers are little-endian.
  • Every QR code carries exactly one frame, encoded as a single byte-mode segment.
  • The first two bytes of a frame are its magic. The first byte alone tells the families apart.
MagicMeaningHeaderSource
D2 0CMode 2 frame, RaptorQ24 bytessrc/mode2.rs
AC 0CAcknowledgement frame19 bytes, no payloadsrc/ack.rs
4B 43 46 31 ("KCF1")File container. A payload format, never a frame58 bytessrc/kcf.rs

Mode 2 is the only forward frame. Any other leading pair sniffs as unknown and is dropped. Each parser rejects the other family at its length or magic check, so a screen showing mixed frames never corrupts either decoder.

The normative specifications are docs/MODE2-SPEC.md and docs/KCF1-SPEC.md in the Air Kalon repository.

Mode 2 frame

A 24-byte header followed by exactly symbolLen payload bytes.

OffsetSizeFieldTypeMeaning
02magicu8 u8D2 0C
21verFlagsu8Low nibble version, must be 1. High nibble flags, must be 0. The byte is always 01
31cipherSuiteu80 is plaintext. Other values are reserved and not supported
42sessionIdu16Random per sender start
64esiu32RaptorQ encoding symbol identifier. Below k is a source symbol, from k up is a repair symbol
102ku16Source symbol count, 1 to 56,403
122symbolLenu16Payload bytes per frame. A multiple of 8 when 64 or more
144totalLenu32Length of the transmitted object, the whole KCF1 container, in bytes
184payloadFnvu32FNV-1a of the whole container, checked once on completion
222reservedu16Must be 00 00
24symbolLensymbolbytesThe encoding symbol

Validation order

The parser runs these checks in this order and returns nothing at the first failure:

  1. Length is more than 24 bytes.
  2. Magic is D2 0C.
  3. verFlags is 01.
  4. k, symbolLen and totalLen are all non-zero.
  5. The reserved field is zero.
  6. Length is exactly 24 + symbolLen.
  7. If symbolLen is 64 or more, it is a multiple of 8.
  8. k is at most 56,403, the RFC 6330 limit for a single source block.
  9. k equals ceil(totalLen / symbolLen).

The cipher suite is deliberately not validated: a frame with a nonzero suite parses, and the decoder reports it as an unsupported suite rather than as garbage. The engine drops any symbol with esi of 2^24 or more, the RFC 6330 payload identifier limit, and refuses to lock on to a stream whose totalLen exceeds 256 MiB.

Derived parameters

No RaptorQ parameters travel on the wire. Both ends compute:

raptorq::ObjectTransmissionInformation::with_defaults(total_len as u64, symbol_len)

Rules 7 to 9 guarantee that this yields a symbol size equal to symbolLen and one source block. The golden vectors pin the standard's 12-byte serialisation of the result for representative sizes, so a library upgrade cannot change it silently.

Stream identity

A stream is identified by (mode, sessionId, k, symbolLen, totalLen, payloadFnv, cipherSuite), where mode is always 2. Every frame of one stream carries identical values for all of them.

Reserved for encryption

Every nonzero cipher suite is reserved for a future authenticated encryption suite. Its associated data is already fixed as the exact 24 transmitted header bytes, so a future suite cannot authenticate less than the whole header, and flipping the suite byte fails the tag. No suite other than 0 is implemented.

Frame capacity

The largest frame is the byte capacity of a version 40 QR code at the chosen error correction level.

Error correctionLargest frame
L2,953 bytes
M2,331 bytes
Q1,663 bytes
H1,273 bytes

At error correction level L:

QR versionCapacityLargest symbolLargest stream (56,403 symbols)
271,465 bytes1,440 bytes81,220,320 bytes, about 77 MiB
402,953 bytes2,928 bytes165,147,984 bytes, about 157 MiB

The header is 1.6 percent of a version 27 frame and 0.8 percent of a version 40 frame.

Acknowledgement frame

Exactly 19 bytes, with no payload.

OffsetSizeFieldTypeMeaning
02magicu8 u8AC 0C
21verFlagsu8Must be 01
32sessionIdu16The session being acknowledged, non-zero
51modeu8Must be 2, the mode of the acknowledged stream
64framesNewu32Distinct frames received so far
102ku16The acknowledged stream's K, non-zero
121doneu80 in progress, 1 complete
134identityFnvu32The acknowledged stream's payloadFnv
172reservedu16Must be 00 00

Checks, in order: exact length 19, magic, verFlags, mode 2, non-zero session and K, done of 0 or 1, reserved zero. framesNew and identityFnv accept any value; whether they match is the sender's decision.

KCF1 file container

Every transfer carries exactly one KCF1 container. The frame layer treats it as opaque bytes: totalLen is the container's length and payloadFnv is the FNV-1a of the container.

Layout

OffsetSizeFieldMeaning
04magicKCF1 (4B 43 46 31)
41version1
51flagsSee below
62nameLenUTF-8 length of the file name, at most 255
82typeLenUTF-8 length of the media type, at most 127
108originalLenLength of the file before compression, at most 256 MiB
188bodyLenLength of the body as carried, at most 256 MiB
2632sha256SHA-256 of the original, uncompressed bytes
58nameLennameFile name
typeLentypeMedia type
bodyLenbodyThe file, raw DEFLATE when flag bit 0 is set

A container is exactly 58 + nameLen + typeLen + bodyLen bytes long.

Flags

BitMaskMeaning
00x01The body is a raw DEFLATE stream (RFC 1951, with no wrapper around it)
10x02The file is a text snippet the sender typed or pasted
20x04The file is a folder bundle: a zip archive of several files
3 to 7Reserved, must be 0

Bits 1 and 2 are mutually exclusive. With neither set the container carries an ordinary file. The kind bits are presentation hints and never change how the body is decoded or verified.

Packing

  1. Name. Take the last path component, splitting on both / and \. Remove every control character and every /, \ and :. Trim leading dots and spaces. Clamp to 255 UTF-8 bytes on a character boundary. An empty result becomes file.
  2. Media type. Clamp to 127 UTF-8 bytes on a character boundary. An empty type becomes application/octet-stream.
  3. Hash. SHA-256 over the original bytes.
  4. Compression. With o the original length and c the raw DEFLATE length, compress only when o - c >= 64 and 20 * (o - c) >= o, that is, when it saves at least 64 bytes and at least 5 percent. Otherwise store the original bytes. A packer may skip the attempt when it can tell it will not pay; the format is the same either way.
  5. Flags. Set bit 0 if the body is compressed, and at most one of bits 1 and 2.

Packing refuses only a file over 256 MiB. An empty file is valid.

Unpacking

A receiver checks these rules in this order and stops at the first one broken. Each has its own error code, as reported through the C interface.

#RuleErrorCode
1Buffer at least 58 bytesTruncated1
2Magic is KCF1BadMagic2
3Version is 1BadVersion3
4Flag bits 3 to 7 are 0UnknownFlags4
5Not both bit 1 and bit 2ConflictingFlags5
6Name length at most 255NameTooLong6
7Media type length at most 127MediaTypeTooLong7
8Original length at most 256 MiBOriginalTooLarge8
9Body length at most 256 MiBBodyTooLarge9
10The four lengths add up to the buffer lengthLengthMismatch10
11Name is valid UTF-8NameNotUtf811
12Media type is valid UTF-8MediaTypeNotUtf812
13Uncompressed: body length equals original lengthStoredLengthMismatch13
14Compressed: the body is one complete raw DEFLATE streamDeflateInvalid14
15Compressed: no bytes follow the end of that streamDeflateTrailingData15
16Compressed: it inflates to exactly the original lengthDecompressedLengthMismatch16
17SHA-256 of the recovered bytes equals the header'sHashMismatch17

Packing's one refusal is FileTooLarge, code 20. A null pointer with a non-zero length, or an unknown kind, is BadArgument, code -1, at the C interface.

Requirements on the unpacker:

  • Bounded inflation. Never produce more than the original length plus one byte while inflating. The reference unpacker grows its output in 1 MiB steps, so a small container that declares a large original length costs nothing until it actually inflates.
  • Re-sanitise. Sanitise the name again with the packing rule after reading it, and read an empty media type as application/octet-stream.
  • Verify before offering. Offer a file only after rule 17 passes.

The SHA-256 covers the file bytes only. The name, media type and flags are covered in transit by the frame header's FNV-1a over the whole container, a transport check rather than an integrity guarantee.

Hash

FNV-1a, 32-bit: offset basis 0x811C9DC5, prime 0x01000193, one byte at a time in wrapping arithmetic. It detects accidental corruption. It is not a cryptographic hash.

Golden vectors and tests

FilePins
tests/golden_mode2.jsonMode 2 frames at edge values, a rejection vector for every validation rule, the RaptorQ engine's packets and parameter serialisation, acknowledgement frames and their rejections, and cross-family rejection in both directions
tests/kcf.rs and the unit tests in src/kcf.rsRound trips, the compression rule at its boundaries, every sanitising rule, one test per unpack rejection, a bounded-inflation test and seeded fuzz-style properties

The golden vectors are generated from this implementation at specification freeze, with test payloads drawn from a fixed-seed SplitMix64 byte source. Every test run regenerates them in memory and compares them with the committed file. If a golden assertion fails, the code is wrong. A vector is never relaxed, and an intentional change to the frame layout is a new version number with new vectors.