UTF-8 is a relatively simple way to encode Unicode codepoints in a variable-width format such that it doesn't easily confuse code that isn't Unicode aware.
UTF-8 overview
- Bytes in the range of 1-0x7F, inclusive, are normally valid
- Bytes with the bit pattern
10XX XXXX
are considered continuation bytes, with the six least significant bits being used to encode part of a codepoint. These must not appear unless they are expected by a preceding byte. - Bytes with the pattern
110X XXXX
expect one continuation byte afterward - Bytes with the pattern
1110 XXXX
expect two continuation bytes afterward - Bytes with the pattern
1111 0XXX
expect three continuation bytes afterward - All other bytes are invalid and should not appear anywhere in a UTF-8 stream. 5, 6, and 7 byte clusters are possible in theory, but will not be allowed for the purposes of this challenge.
Overlong encodings
UTF-8 also requires that a codepoint should be represented with the minimum number of bytes. Any byte sequence that could be represented with fewer bytes is not valid. Modified UTF-8 adds one exception to this for null characters (U+0000), which should be represented as C0 80
(hex representation)), and instead disallows null bytes to appear anywhere in the stream. (This makes it compatible with null-terminated strings)
Challenge
You are to make a program that, when given a string of bytes, will determine if that string represents valid Modified UTF-8 and will return a truthy value if valid and a falsy value otherwise. Note that you must check for overlong encodings and null bytes (since this is Modified UTF-8). You do not need to decode the UTF-8 values.
Examples
41 42 43 ==> yes (all bytes are in the 0-0x7F range)
00 01 02 ==> no (there is a null byte in the stream)
80 7F 41 ==> no (there is a continuation byte without a starter byte)
D9 84 10 ==> yes (the correct number of continuation bytes follow a starter byte)
F0 81 82 41 ==> no (there are not enough continuation bytes after F0)
EF 8A A7 91 ==> no (too many continuation bytes)
E1 E1 01 ==> no (starter byte where a continuation byte is expected)
E0 80 87 ==> no (overlong encoding)
41 C0 80 ==> yes (null byte encoded with the only legal overlong encoding)
F8 42 43 ==> no (invalid byte 'F8')
Rules
- Standard rules and loopholes apply
- Input and output can be in any convenient format as long as all values in the unsigned byte range (0-255) can be read in.
- You may need to use an array or file rather than a null-terminated string. You need to be able to read null bytes.
- Shortest code wins!
- Note that using builtins to decode the UTF-8 is not guaranteed to conform to the requirements given here. You may need to work around it and create special cases.
EDIT: added bonus for not using builtins that decode UTF-8
EDIT2: removed bonus since only the Rust answer qualified and it's awkward to define.