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

Запись файлов и liza-copy

Реализовано в 0.0.25. Файловая запись — внешний эффект с возможным частичным результатом. Ошибка не откатывает файл. Полученные File остаются move-only ресурсами, которые требуется закрыть на всех нормальных путях.

Создать новый файл

file.create_path(path) // Result<File,FileCreateError>, io

path: Path сохраняет платформенное представление: байты на POSIX, UTF-16 на Windows. Успех создаёт новый пустой обычный файл, открытый только для записи. Существующий destination не усечётся и не будет открыт для записи. Проверка и создание выполняются одной операцией ОС, без предварительного «проверить существование, затем открыть».

enum FileCreateError { AlreadyExists; Failure(FileError) }

AlreadyExists означает отказ ОС из-за существующего имени. Другие ошибки оборачиваются в Failure с прежним FileError. Его набор вариантов не расширен: существующие исчерпывающие match остаются допустимыми. Проверки доступа и допустимости имени по-прежнему выполняет ОС.

На POSIX используются O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, права 0666 ограничиваются текущим umask. Существующая конечная symlink, включая висячую, не разыменовывается для перезаписи. На Windows используется CreateFileW с CREATE_NEW, GENERIC_WRITE и FILE_SHARE_READ. Права и метаданные source не копируются; отсутствующие каталоги не создаются.

Перезапись, append, атомарная замена, удаление и нормализация пути в этот API не входят. Произвольные конкурентные изменения файловой системы другим процессом не превращаются в изолированную транзакцию.

Записать чанк

file.write_chunk(output, data) // Result<u32,FileWriteFailure>, io

output: File заимствуется на время вызова, а не потребляется. data: View<u8> предоставляет readonly-доступ к байтам; runtime не сохраняет этот view. После вызова файл по-прежнему обязан быть закрыт. Запись идёт с текущей позиции файла и продвигает её на успешно записанные байты.

enum FileWriteError {
    InvalidHandle; PermissionDenied; NoSpace; WouldBlock;
    WriteZero; TooLarge; Other
}
struct FileWriteFailure { kind:FileWriteError; written:u32 }

Это справочные объявления библиотечных типов, не код для повторного объявления.

  • Успех: Ok(data.len), все байты приняты ОС.
  • Короткая успешная системная запись: runtime продолжает с оставшегося суффикса.
  • Ошибка: Err содержит причину и written — подтверждённое число байтов за этот вызов, не общий размер файла и не число байтов в долговременном хранилище.
  • Успешная системная запись нуля байтов при непустом остатке: WriteZero, без бесконечного повторения.
  • Пустой view: Ok(0) без системного вызова и изменения позиции.

Каждый запрос к ОС ограничен 1 МиБ; длина view остаётся u32. На POSIX EINTR до записи байтов повторяется. Windows учитывает число байтов, сообщённое WriteFile, в том числе при отказе. Режим асинхронного I/O не используется.

NoSpace включает исчерпание места/дисковой квоты, TooLarge — файловый лимит размера. Конкретные ошибки доступа и допустимости операции зависят от ОС; например, передача файла, открытого только для чтения, не даёт права записи. Тип File пока не разделён на отдельные типы прав доступа.

POSIX runtime временно блокирует SIGPIPE и SIGXFSZ в вызывающем потоке, чтобы соответствующий отказ можно было вернуть как значение. Он восстанавливает маску и сохраняет сигналы, уже ожидавшие обработки до вызова. Диспозиции сигналов процесса не изменяются.

При ошибке подтверждённый префикс не откатывается; другие изменения внешнего состояния не исключаются. Повторная запись всего view после частичного успеха может продублировать префикс. Автоматического восстановления после ошибки или возврата файловой позиции нет.

Синхронизация и закрытие

file.flush(output)      // Result<u32,FileWriteError>, io; borrow
file.close(move output) // Result<u32,FileError>, io; consume

flush запрашивает синхронизацию данных и метаданных файла через fsync на POSIX или FlushFileBuffers на Windows. Успех возвращает Ok(0). Операция не закрывает файл и не снимает обязанность владения. EINTR для POSIX-синхронизации повторяется. Она предназначена для записываемого файла; доступность операции для других дескрипторов определяется ОС.

Успешная запись означает приём данных ОС, а не гарантированное сохранение после отключения питания. Успешный flush означает выполнение запроса синхронизации по контракту ОС/файловой системы. Это не универсальная защита от аппаратных сбоев, не атомарный commit и не синхронизация каталога, содержащего новое имя. В частности, liza-copy не обещает crash-atomic публикацию destination.

close потребляет ресурс даже при ошибке. Повторять закрытие через ту же привязку нельзя. На ошибочном пути запись/flush не освобождают от попытки закрыть файл; при trap пользовательский cleanup по-прежнему не обещается.

Платформенные основания: POSIX open, write, fsync, Windows WriteFile и FlushFileBuffers.

liza-copy

python3 -m liza build examples/apps/liza_copy.liza -o build/liza-copy
./build/liza-copy source.bin destination.bin

На Windows результат — build/liza-copy.exe. Через компилятор:

python3 -m liza run examples/apps/liza_copy.liza -- source.bin destination.bin

Destination должен отсутствовать. Source открывается первым; если destination существует, в том числе является source или hard link на него, утилита отказывает без усечения. Программа копирует содержимое бинарно, включая NUL, невалидный UTF-8 и любые переводы строк. Не выполняются преобразования текста, копирование атрибутов, рекурсивный обход или чтение stdin.

Порядок: чтение по 2 КиБ → полная запись чанка → повторение до EOF → flush destination → закрытие destination → закрытие source. Объём памяти не растёт с размером файла. После ошибки чтения/записи дальнейший перенос прекращается, flush пропускается, но оба ресурса закрываются.

Код Значение Потоки
0 Всё скопировано, синхронизировано и закрыто stdout/stderr пусты
2 Неверное число аргументов stderr: Usage: liza-copy <source> <destination>\n
3 Ошибка открытия, создания, чтения, записи, flush, close или диагностики stdout пуст; stderr по возможности сообщает причину

\n означает один LF. Для существующего destination:

Destination already exists

Для ошибки после частичной записи:

Cannot write destination; bytes written in current chunk: 32
Destination may be incomplete; it has not been removed

Первичная причина сохраняется. Дополнительные ошибки закрытия выводятся после неё как Also failed to close destination / Also failed to close source. Оба ресурса потребляются до вывода диагностики, поэтому даже отказ stderr не пропускает cleanup. При отказе записи stderr сообщение может быть неполным; код остаётся 3.

После ошибки, произошедшей после успешного создания, файл не удаляется. Он может быть пустым, частично записанным или полным, но без подтверждённой синхронизации/закрытия. Последующий запуск с тем же destination вновь откажет из-за существующего имени. Решение о сохранении или удалении остаётся у пользователя. При неудаче открытия source или создания destination предупреждение о частичном файле не выводится.

Исходник и проверки стоимости

examples/apps/liza_copy.liza
// A new destination only. Never overwrite or silently remove a partial output.
fn note(text:Text)->u32:io {
    match console.errln(text) {Ok(n)=>{return 0} Err(e)=>{return 3}}
}

fn transfer(input:File, output:File)->u32:io {
    var stage:u32=0
    var written:u32=0
    while true {
        match file.read_chunk(input) {
            Err(error)=>{stage=1;break}
            Ok(chunk)=>{
                if chunk.eof {break}
                bytes=chunk.data
                data=view bytes[0..chunk.len]
                match file.write_chunk(output,data) {
                    Ok(count)=>{}
                    Err(error)=>{stage=2;written=error.written;break}
                }
            }
        }
    }
    if stage==0 {
        match file.flush(output) {Ok(done)=>{} Err(error)=>{stage=3}}
    }
    var output_close_failed=false
    var input_close_failed=false
    match file.close(move output) {
        Ok(done)=>{}
        Err(error)=>{output_close_failed=true;if stage==0 {stage=4}}
    }
    match file.close(move input) {
        Ok(done)=>{}
        Err(error)=>{input_close_failed=true;if stage==0 {stage=5}}
    }
    // Both resources are consumed before diagnostics, even if stderr fails.
    if stage==0 {return 0}
    if stage==1 {reported=note("Cannot read source")}
    if stage==2 {
        a=console.err("Cannot write destination; bytes written in current chunk: ")
        b=console.err_u64(u64(written))
        c=console.errln("")
    }
    if stage==3 {reported=note("Cannot flush destination")}
    if stage==4 {reported=note("Cannot close destination")}
    if stage==5 {reported=note("Cannot close source")}
    if output_close_failed && stage!=4 {reported=note("Also failed to close destination")}
    if input_close_failed && stage!=5 {reported=note("Also failed to close source")}
    reported=note("Destination may be incomplete; it has not been removed")
    return 3
}

fn copy_file(source:Path, destination:Path)->u32:io {
    match file.open_path(source) {
        Err(error)=>{reported=note("Cannot open source");return 3}
        Ok(input)=>{
            match file.create_path(destination) {
                Ok(output)=>{return transfer(move input,move output)}
                Err(error)=>{
                    var close_failed=false
                    match file.close(move input) {Ok(done)=>{} Err(e)=>{close_failed=true}}
                    match error {
                        FileCreateError.AlreadyExists=>{reported=note("Destination already exists")}
                        FileCreateError.Failure(cause)=>{reported=note("Cannot create destination")}
                    }
                    if close_failed {reported=note("Also failed to close source")}
                    return 3
                }
            }
        }
    }
}

fn usage()->u32:io {
    result=note("Usage: liza-copy <source> <destination>")
    if result!=0 {return result}
    return 2
}

fn main()->u32:io {
    if cli.count()!=3 {return usage()}
    match cli.path_arg(1) {
        None=>{return usage()}
        Some(source)=>{
            match cli.path_arg(2) {
                None=>{return usage()}
                Some(destination)=>{return copy_file(source,destination)}
            }
        }
    }
}

file.write_chunk не создаёт дополнительный буфер содержимого и не выделяет heap-память. Он передаёт указатель на текущий суффикс view платформенной реализации записи. Это не обещание zero-copy внутри ОС или отсутствия всех копий фиксированного значения FileChunk при чтении и передаче между функциями.

O0/O2 проверяют точные бинарные копии, пустые и большие файлы, защиту существующих имён, короткие/нулевые записи, отказ после префикса и ошибки синхронизации/закрытия. POSIX-проверка с реальным файловым лимитом подтверждает возврат ошибки, сохранение 32 записанных байтов и отсутствие усечения при повторном запуске. Инъекция ошибок проверяет порядок и однократное закрытие обоих ресурсов, включая отказ stderr. Отдельно проверяются исходный адрес байтов при коротких записях и отсутствие аллокаций/копирования содержимого в выбранном тракте LLVM IR и машинного кода.