// lesson: utf8

Text Is Not Bytes โ€” UTF-8

Everything so far pretended one byte = one character. For 1970s terminals that was true; for yours it can't be โ€” type รฉ, โ†’, or ๆ—ฅ into any modern shell and multiple bytes flow. Modern terminals are UTF-8 native: it's what programs write to them and what keyboards send from them. A terminal that mishandles it doesn't get to call itself one.

The design (worth knowing the story)

In 1992, needing an encoding for Plan 9, Ken Thompson sketched UTF-8 on a New Jersey diner placemat (Rob Pike's telling of it is in the extended reading, and it's a great read). The constraints: encode all of Unicode; keep ASCII files valid unchanged; never produce a 0x00 or other ASCII byte inside a multi-byte character (so C strings, /-separated paths, and every existing Unix tool keep working); and make it possible to find character boundaries from anywhere in a stream. The result:

Code point range Bytes Pattern
U+0000 โ€“ U+007F 1 0xxxxxxx
U+0080 โ€“ U+07FF 2 110xxxxx 10xxxxxx
U+0800 โ€“ U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
U+10000 โ€“ U+10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

The payload bits (x) carry the code point, big-end first. Read the patterns as a self-describing header: the number of leading 1-bits in the first byte is the sequence length, and every continuation byte is unmistakable (10xxxxxx, values 0x80โ€“0xBF). Consequences:

  • ASCII is already UTF-8. Every file you've ever compiled: valid.
  • Self-synchronizing. Land at a random byte and you can find the next character start without backing up: skip continuation bytes.
  • No aliasing with ASCII. A multi-byte character contains no bytes < 0x80 โ€” strchr(s, '/') can't match inside one.

Worked example โ€” รฉ is U+00E9, binary 000 1110 1001 (11 bits โ†’ needs the 2-byte form's 11 payload slots):

110 00011   10 101001
    ^^^^^      ^^^^^^
    00011      101001   โ†’  0xC3 0xA9

Decoding, and the ways bytes go wrong

A decoder collects a leading byte, then the right number of continuation bytes, and reassembles:

cp = (lead & payload_mask);
for each continuation byte b:  cp = (cp << 6) | (b & 0x3F);

But terminals eat arbitrary byte streams, so the error cases are not optional:

  • A stray continuation byte (0x80โ€“0xBF with no leading byte).
  • A leading byte followed by a non-continuation โ€” the sequence was truncated by whoever produced it.
  • Overlong encodings: 0xC0 0x80 decodes by-the-rules to U+0000 โ€” the 2-byte form of a 1-byte value. Forbidden by the spec, and historically a real security hole (encode / as 0xC0 0xAF and sail past a path check that only looked for byte 0x2F). Reject: 2-byte forms below U+0080, 3-byte below U+0800, 4-byte below U+10000.
  • UTF-16 surrogates (U+D800โ€“U+DFFF) and values past U+10FFFF: not characters; reject.

The universal convention for all of these โ€” what every terminal, browser, and editor does โ€” is to emit U+FFFD REPLACEMENT CHARACTER (๏ฟฝ), consume a minimal prefix (we'll consume exactly one byte), and carry on. Never stall, never crash, never let one bad byte eat good ones after it.

There's one more case that is not an error: the buffer simply ends mid-character because read() split it. Same answer as the VT parser and the key decoder: return "need more bytes" and let the caller re-present the tail later. (Sensing the pattern? Incremental interfaces over byte buffers, all the way down. They compose: GROUND-state print bytes flow from the VT parser into the UTF-8 decoder, whose code points land in screen cells.)

One honest caveat before the challenge: code point โ‰  column. ๆ—ฅ occupies two terminal columns; combining marks occupy zero; that's the wcwidth() problem, and real emulators carry Unicode tables for it. Our editor sticks to one-column characters, but know the dragon is there.

โ€บ A UTF-8 Decoder

20 pts

Implement:

size_t utf8_decode(const unsigned char *buf, size_t len, uint32_t *cp);
  • Decode the character starting at buf[0]; store the code point in *cp; return the number of bytes consumed (1โ€“4).
  • If the buffer ends mid-character (valid prefix, not enough bytes): return 0 โ€” need more input.
  • On invalid input (stray continuation, bad follow byte, overlong, surrogate, > U+10FFFF, or an invalid leading byte like 0xFE): store 0xFFFD and return 1 โ€” consume the offending byte only, so decoding can resynchronize.

Also implement utf8_encode(cp, out) โ€” the reverse, needed when the editor saves multi-byte text โ€” returning the byte length (and encoding invalid code points as U+FFFD).

Log in to submit a solution and earn points.