Перейти к содержанию

Потоковая проверка UTF-8

Реализовано в 0.0.23. utf8_stream проверяет текст и считает кодовые точки независимо от разбиения входа на чанки. Кодировка одной кодовой точки может начинаться в одном чтении и заканчиваться в следующем. Библиотека написана на самой Liza, без аллокаций и копирования входных чанков.

Правила корректности кодирования совпадают с UTF-8 API: NUL, BOM и noncharacters допустимы; избыточные кодировки, суррогаты, неверные и усечённые последовательности отклоняются. Считаются Unicode scalar values, не графемы и не видимые символы.

API

state = utf8_stream.create()
next = utf8_stream.feed(state, chunk)
result = utf8_stream.finish(next)
Функция Тип результата Контракт
utf8_stream.create() Utf8StreamState Пустой поток, позиция и счётчик равны нулю
utf8_stream.feed(state, data:View<u8>) Utf8StreamState Новое состояние после обработки чанка
utf8_stream.finish(state) Result<u64,Utf8StreamError> Число завершённых кодовых точек либо ошибка

Все функции pure. Namespace доступен без import. Аргументы вычисляются слева направо по обычным правилам языка.

Utf8StreamState — небольшое копируемое значение с приватными полями. Создать его можно через create; произвольное конструирование, чтение или обновление внутренних полей недоступны. Обычная передача копирует логическое состояние. copy позволяет сохранить независимый снимок, move работает по обычным правилам владения. Пользовательские функции и generics могут передавать и возвращать состояние.

В состоянии нет view, адреса чанка или зависимости от региона. Оно может пережить исходные байты. Незавершённая кодировка представлена числом оставшихся байтов и допустимым диапазоном следующего байта; хранить сами байты не нужно. Выдача декодированных кодовых точек потребителю пока не входит в API.

Чанки и EOF

feed не считает конец чанка концом текста. Пустой чанк ничего не меняет. Незавершённая последовательность переносится в следующее состояние, а её ведущий байт сохраняет абсолютное смещение относительно начала потока.

finish рассматривает текущее состояние как конец текста:

  • без ошибки и незавершённой кодировки возвращает Ok(count);
  • при незавершённой кодировке возвращает InvalidEncoding на её начале;
  • при ранее обнаруженной ошибке возвращает эту ошибку.

finish не изменяет состояние и не «закрывает» декодер. Его можно вызывать повторно, затем продолжить поток через feed. Например, после первого байта трёхбайтовой кодировки finish сообщает об усечении; после получения остальных байтов проверка того же продолжаемого потока может завершиться успешно. Чтобы начать независимый поток, вызывайте create.

Если feed обнаружил некорректный байт или переполнение, ошибка становится постоянной для возвращённого состояния. Следующие feed, включая пустые, сохраняют её и не читают содержимое чанка. Вычисление аргументов вызова при этом по-прежнему происходит. Независимый снимок прежнего состояния не изменяется.

Ошибки и ограничения размера

enum Utf8StreamErrorKind { InvalidEncoding; SizeOverflow }
struct Utf8StreamError { kind:Utf8StreamErrorKind; offset:u64 }

Это справочные объявления библиотечных типов. Повторно объявлять их не нужно.

InvalidEncoding.offset — абсолютное байтовое смещение начала первой некорректной последовательности. Ведущий байт может находиться в предыдущем чанке. Для усечённой последовательности при EOF правило то же.

Позиция и число завершённых кодовых точек представлены u64, длина одного view остаётся u32. Максимальная поддерживаемая длина потока — 2^64−1 байт. Если следующий байт потребовал бы увеличить позицию за этот предел, feed возвращает состояние с SizeOverflow и offset = 2^64−1, не читая этот байт. Счётчик кодовых точек также проверяется перед увеличением; для доступных через публичный API состояний он не превосходит число обработанных байтов.

Проверка предела выполняется перед каждым байтом, а не для всего чанка заранее. Поэтому ошибка кодирования раньше предела имеет приоритет над более поздним переполнением независимо от разбиения на чанки. Незавершённая кодировка на пределе даёт InvalidEncoding при finish; попытка продолжить такой поток через непустой feed даёт SizeOverflow.

Ожидаемые ошибки текста и размера не вызывают trap. Обязательные traps при вычислении аргументов сохраняются.

Стоимость

create и finish выполняют O(1) работы. feed — O(n) по размеру чанка до первой ошибки, O(1) для уже ошибочного состояния. Дополнительная память O(1). Копирование небольшого состояния допустимо; входной чанк не материализуется в новом буфере. Нет скрытого роста, heap-аллокаций или зависимостей от чанка.

Результат проверки одного набора байтов не зависит от размеров чанков и количества пустых чанков. Повторный finish не меняет результат будущего feed.

Пример: файл до EOF

python3 -m liza run examples/utf8_stream_file.liza

Программа читает examples/data/utf8.txt до EOF через file.read_chunk. 2 КиБ — предел одного чтения, а не размера файла. Содержимое файла можно заменить для проверки большого текста. Начальный пример выводит Code points: 7.

examples/utf8_stream_file.liza
// Run from the repository root. Each read is bounded, the file is not.
// Exit codes: 1 open, 2 read, 3 output, 4 close, 5 UTF-8, 6 size overflow.
fn report(state:Utf8StreamState)->u32:io {
    match utf8_stream.finish(state) {
        Ok(count)=>{
            match console.write("Code points: ") {Ok(n)=>{} Err(e)=>{return 3}}
            match console.print_u64(count) {Ok(n)=>{} Err(e)=>{return 3}}
            match console.println("") {Ok(n)=>{} Err(e)=>{return 3}}
            return 0
        }
        Err(error)=>{
            match error.kind {
                Utf8StreamErrorKind.SizeOverflow=>{return 6}
                Utf8StreamErrorKind.InvalidEncoding=>{
                    match console.write("Invalid UTF-8 at byte: ") {Ok(n)=>{} Err(e)=>{return 3}}
                    match console.print_u64(error.offset) {Ok(n)=>{} Err(e)=>{return 3}}
                    match console.println("") {Ok(n)=>{} Err(e)=>{return 3}}
                    return 5
                }
            }
        }
    }
}

fn main()->u32:io {
    match file.open("examples/data/utf8.txt") {
        Err(error)=>{return 1}
        Ok(input)=>{
            var state=utf8_stream.create()
            var status:u32=0
            while true {
                match file.read_chunk(input) {
                    Err(error)=>{status=2;break}
                    Ok(chunk)=>{
                        if chunk.eof {break}
                        bytes=chunk.data
                        data=view bytes[0..chunk.len]
                        state=utf8_stream.feed(state,data)
                    }
                }
            }
            // Close before reporting, including on a read failure.
            match file.close(move input) {
                Err(error)=>{if status==0 {return 4}}
                Ok(closed)=>{}
            }
            if status!=0 {return status}
            return report(state)
        }
    }
}

Состояние создаётся один раз перед циклом. Каждая итерация владеет отдельным чанком; data ограничивает view фактической длиной чтения. Новое состояние сохраняет только сведения о проверке и не зависит от времени жизни data.

Только chunk.eof заканчивает чтение. После цикла программа явно закрывает File, включая путь ошибки чтения. finish и вывод результата выполняются после успешного чтения и закрытия.

Коды выхода: 0 — успех; 1 — открытие; 2 — чтение; 3 — вывод; 4 — закрытие; 5 — некорректный UTF-8; 6 — предел размера. Ошибка чтения имеет приоритет над ошибкой закрытия. Пример дочитывает файл до EOF даже после обнаружения ошибки кодирования; чтение остаётся отдельной операцией с эффектом io и собственной стоимостью.

Проверки

O0/O2 сверяются с независимым декодером Python: все пары байтов целиком и раздельно, все разрывы граничных кодировок, случайные разбиения и поток всех Unicode scalar values чанками 2047/2048/2049 байт. Проверяются пустые чанки, EOF, первая ошибка, независимые снимки, приватность состояния и пределы u64 через тестовое конструирование внутренних состояний.

Файловый пример проверяется на входах больше 2 КиБ, включая разрыв кодировки на границе чтения. В выбранном тракте с runtime-входом LLVM IR и машинный код проверяются на отсутствие вызовов аллокации и массового копирования чанка.