UTF-8: байты, кодовые точки и диапазоны
Реализовано в 0.0.22. Библиотека utf8 читает View<u8> обычными
pure-функциями, написанными на Liza. Входом может служить view массива,
регионального буфера или Vec<u8>. Владелец и его заимствование сохраняются;
функции не выделяют память и не копируют входные байты.
Это минимальные текстовые операции. Изменяемый String, нормализация,
регистровые преобразования и графемные кластеры пока не реализованы.
Существующий Text для литералов и консоли остаётся отдельным типом;
неявного преобразования Text в View<u8> нет.
Три разных единицы
- Байтовое смещение
u32адресует позицию в переданном view, начиная с нуля. - Кодовая точка в корректном UTF-8 — Unicode scalar value, представленное
одним–четырьмя байтами. Декодер возвращает его числовое значение
u32. - Видимый символ может состоять из нескольких кодовых точек.
Например,
eи следующий комбинируемый акцент — две кодовые точки.
utf8.count считает кодовые точки, не байты, не графемы и не ширину текста
на экране. Пустой текст корректен; NUL, BOM и Unicode noncharacters допустимы.
NUL не завершает текст, BOM не удаляется и учитывается при подсчёте.
Правила кодирования соответствуют
Unicode, §3.9, таблице 3-7:
декодер отвергает неверные ведущие и продолжающие байты, усечённые и
избыточные кодировки, суррогаты и значения выше U+10FFFF.
API и стоимость
Namespace utf8 доступен без import, как console и file.
Все аргументы вычисляются по обычным правилам вызова, слева направо.
| Вызов | Результат | Работа с байтами |
|---|---|---|
utf8.validate(data) |
Result<Utf8Done,Utf8Error> |
Проверяет весь вход до первой ошибки, O(n) |
utf8.count(data) |
Result<u32,Utf8Error> |
Проверяет и считает за один проход, O(n) |
utf8.next(data,index) |
Result<Option<Utf8Step>,Utf8Error> |
Декодирует одну последовательность, O(1) |
utf8.is_boundary(data,index) |
Result<bool,Utf8Error> |
Проверяет весь вход, затем границу, O(n) |
utf8.range(data,start,end) |
Result<Utf8Range,Utf8Error> |
Проверяет весь вход и обе границы, O(n) |
Дополнительная память всех операций — O(1). n — длина входа в байтах.
Результаты — небольшие независимые значения, которые могут пережить владельца
байтов. Вызовы не возвращают view и не продлевают время заимствования.
struct Utf8Done {}
struct Utf8Step { value:u32; next:u32 }
struct Utf8Range { start:u32; end:u32 }
enum Utf8ErrorKind { InvalidEncoding; OutOfBounds; NotBoundary; ReversedRange }
struct Utf8Error { kind:Utf8ErrorKind; offset:u32 }
Это объявления библиотечных типов для справки; повторно объявлять их в
программе не нужно. Utf8Range — пара чисел, а не сохраняемое доказательство
валидности: после изменения владельца прежняя проверка ничего не гарантирует.
Декодирование и диагностика
next проверяет только последовательность, начинающуюся в index.
Он не проверяет префикс или оставшийся суффикс:
index == data.lenвозвращаетOk(None), даже если предшествующие байты некорректны;index > data.lenвозвращаетOutOfBoundsсо смещениемindex;- начало внутри многобайтовой последовательности возвращает
InvalidEncoding; - успех возвращает
Some(Utf8Step(value,next)), гдеnext > index.
Чтобы проверить текст целиком, используйте validate, count или проход
через next, начиная с нуля и продолжая до конца.
InvalidEncoding.offset — начало первой некорректной последовательности,
а не обязательно позиция неверного continuation byte. Например, для
[65, 226, 40, 161] ошибка начинается на байте 1. Для усечённой
последовательности также возвращается позиция её ведущего байта.
Все смещения отсчитываются от начала переданного view, не исходного файла.
Некорректный UTF-8 и недопустимые диапазоны возвращают Err, а не trap.
Обязательные traps при вычислении аргументов по-прежнему сохраняются.
Границы и диапазоны
На корректном тексте 0 и data.len всегда являются границами.
Позиция между байтами одной кодовой точки — не граница.
is_boundary сначала проверяет index <= len, затем валидность всего
входа и только после этого возвращает true или false.
Даже запрос границы 0 не скрывает некорректный суффикс.
Для range порядок проверок определён:
start > end→ReversedRange, смещениеstart.end > len→OutOfBounds, смещениеend.- Некорректный UTF-8 где-либо во входе →
InvalidEncoding. startвнутри кодировки →NotBoundary, смещениеstart.endвнутри кодировки →NotBoundary, смещениеend.- Иначе
Ok(Utf8Range(start,end)).
Диапазон полуоткрытый: [start,end). Пустой диапазон допустим только на
границе кодовой точки. Это проверка диапазона, а не создание среза через
другой view: ограничение на reborrow сохраняется.
Пример: файл → view → текст
Файл examples/data/utf8.txt содержит Лиза 😀 и перевод строки:
14 байтов, 7 кодовых точек. Программа выводит Code points: 7.
Файл читается через текущий bounded API file.read, поэтому ограничен
2048 байтами. Это не потоковый декодер: разрыв кодовой точки между чанками
поддерживается отдельной потоковой библиотекой 0.0.23.
file.read возвращает Result: ошибка чтения даёт код выхода 1.
В успешной ветви bytes владеет массивом, а data ограничивает доступ
реальной длиной файла, исключая заполненный нулями хвост буфера.
utf8.count(data) одновременно проверяет кодирование и считает кодовые точки.
Ошибка текста выводит байтовое смещение и даёт код 2; ошибка консоли — 3.
Успех возвращает 0. Передача data в декодер не копирует массив.
Проверяемые свойства
На O0/O2 проверяются все одно- и двухбайтовые входы, поток всех Unicode
scalar values, граничные кодировки, случайные входы, смещения и диапазоны.
Результаты сверяются с независимым UTF-8 декодером Python. Отдельно проверяются
views регионального Vec, области заимствования и пример чтения файла.
В проверенном тракте с runtime-входом LLVM IR и машинный код содержат чтение байтов без вызовов аллокации и массового копирования входа. Это проверка текстовых функций; файловый ввод и консоль имеют собственные эффекты и стоимость.