Файловая система: капабилити, линейные дескрипторы, детерминированные сбои
Когда программа открывает файл, операционная система выдаёт ей файловый дескриптор — небольшое целое число, которое обозначает открытый файл внутри процесса, пока программа его не закроет. Дескрипторов ограниченное количество, и долгоживущий сервер, который забывает их закрывать, рано или поздно не сможет открыть ничего. Поэтому каждый открытый файл — обещание его закрыть, и в каждом языке есть свой способ это обещание не совсем сдержать. C полагается на комментарий. C++ и Rust вызывают деструктор, которого вы не видите. kola применяет к файловой системе правило из главы Ресурсы и линейность: дескриптор файла — обычный ресурс, он должен быть израсходован ровно один раз на каждом пути, и программа, которая забыла закрыть файл, не компилируется.
Глобальной файловой системы нет
Функции open(path), которую можно вызвать откуда угодно, не существует. В kola нет полномочий «из воздуха», и файловая система — такая же капабилити, как часы или источник случайности. В работающей программе это sys.fs с корнем в рабочем каталоге. Под kola test это sim.fs, детерминированная файловая система в памяти, о которой речь пойдёт в конце страницы. Функция, которая не получила Fs (или производный от него Dir), не может тронуть ни один файл.
Сами базовые операции намеренно ничем не примечательны. open_read, create, open_append, open_rw и create_rw возвращают File, а read, write, sync и close работают с ним:
fn main(sys: Sys) -> u8 !{io, block, alloc(heap)} {
let mut f = sys.fs.create("greeting.txt").unwrap();
let _w = f.write(&b"hello, kola");
let _s = f.sync().unwrap();
let _c = f.close();
0
}open_rw открывает существующий файл на чтение и запись и отвечает NotFound, если файла нет; create_rw при необходимости его создаёт. Ни та, ни другая не обрезают файл. create обрезает.
Успешный write ещё не значит, что байты на диске. Сначала операционная система копирует их в страничный кеш — область памяти, где она держит недавно использованные данные файлов, — и записывает их позже. Если питание пропадёт раньше, данные потеряются. sync (под ним системный вызов fsync) ждёт, пока данные файла действительно дойдут до накопителя. Он отвечает Result[(), (FsErr, OsErr)] и при любом исходе оставляет дескриптор у вас. Его расходующий двойник, sync_or_close(self) -> Result[File, (FsErr, OsErr)], возвращает файл только при успехе, а при неудаче закрывает его. Это важно, потому что повторять неудавшийся fsync небезопасно: в Linux ядро могло уже выбросить незаписанные данные и сообщить об ошибке один раз, так что второй fsync может пройти успешно, хотя данные на диск так и не попали. С sync_or_close такой повтор просто не написать.
У File есть и позиционный ввод-вывод (read_at, write_at с явным смещением u64 и без общего курсора), size, truncate, dup, рекомендательные блокировки, расширенные атрибуты и прямой ввод-вывод. Со стороны путей есть mkdir, remove, remove_dir, rename, read_dir, stat и is_private. Полный список с сигнатурами — в разделах Filesystem и File в STD-CARD (kola help std-card). Здесь речь о том, как всё это устроено.
Количество байтов, прошедших через ваш буфер, имеет тип usize, потому что им этот буфер нарезают (buf[0..n]). Смещения, длины и размеры самого файла — u64.
File — линейный ресурс
Уберите из программы выше строки с sync и close, и она перестанет компилироваться:
fn main(sys: Sys) -> u8 !{io, block, alloc(heap)} {
let mut f = sys.fs.create("greeting.txt").unwrap();
let _w = f.write(&b"hello, kola");
0
}error[card §2]: resource `f` is not consumed on this path
--> greeting.kl:2:5
|
2 | let mut f = sys.fs.create("greeting.txt").unwrap();
| ^^^
= help: no implicit destructors: move it on, return it, destructure it, or pass it to a consuming fnЭто то же сообщение, что и для любого ресурса, объявленного пользователем. Утёкший файловый дескриптор — не особый случай, о котором чекер знает отдельно. Каждый File, Tmp, Sealed и FsNode должен быть закрыт, запечатан, установлен или брошен на каждом пути, включая пути с ошибкой и ранние возвраты. Вот write_sync из std::fs:
pub fn write_sync(fs: &Fs, path: &str, data: &[u8]) -> Result[(), (FsErr, OsErr)] !{io, block} {
let mut f = fs.create(path)?;
match f.write(data) {
Ok(_) => {}
Err(e) => { let _c = f.close(); return Err(e); }
}
let f = f.sync_or_close()?;
f.close()
}? после fs.create безопасен, потому что живых ресурсов ещё нет. После write файл f жив, и голый ? в этом месте привёл бы к утечке — чекер его не пропустит. Ветка с ошибкой закрывает файл перед возвратом. ? после sync_or_close снова безопасен: при неудаче этот вызов уже закрыл файл.
Чтение файла целиком
f.read(&mut buf) заполняет один буфер и сообщает, сколько байтов получено. Для частого случая, когда нужен весь файл, std::fs::read_all повторяет этот вызов в цикле и дописывает данные в переданный вами Vec[u8]:
use std::fs;
test read_all_whole_file (mut sim: Sim) {
region r {
let data = alloc in r [107u8, 111u8, 108u8, 97u8]; // "kola"
let _w = fs::write_sync(&sim.fs, "greeting.txt", data);
let out: &r mut Vec[u8] = alloc in r mut Vec::new();
match fs::read_all(&sim.fs, "greeting.txt", out) {
Ok(n) => { assert n == 4usize; }
Err(_) => { assert false; }
}
}
}read_all читает кусками по 512 байт, но сохраняет каждый кусок, поэтому пиковое потребление памяти равно размеру файла, и ограничения на размер нет. Он годится для конфигурационных файлов и небольших данных. Если не хочется держать всё содержимое в одном регионе разом, вызывайте f.read(&mut chunk) сами и обрабатывайте каждый кусок по мере поступления или сначала узнайте размер через stat и отказывайтесь от слишком больших файлов. У вспомогательных функций есть двойники для выданных корней: write_sync_in, install_bytes_in, read_all_in и read_all_in_ro.
stat: размер, время и вид без открытия
fs.stat(path) отвечает Result[FileInfo, (FsErr, OsErr)] со структурой FileInfo { size: u64, mtime_ns: u64, kind: FileKind } и не создаёт дескриптора, который потом пришлось бы закрывать. Он есть у Fs, Dir и DirRo.
Варианты FileKind названы по вопросу, который задаёт вызывающий, а не по типам файлов POSIX. Extent — объект фиксированного размера с произвольным доступом (обычный файл или блочное устройство). Stream может блокироваться и осмысленного размера не имеет (символьное устройство, FIFO, сокет). Dir — каталог. Всё, что нельзя доказуемо отнести к одному из них, — Other. Каталог отвечает Ok с kind == Dir и size == 0. Для блочного устройства size — длина устройства, а не ноль, который сообщает st_size. В Darwin эта длина доступна, только если устройство удаётся открыть, иначе она равна 0; это единственный случай, когда size == 0 не означает «пусто».
Extent говорит об адресации, а не о задержках: обычный файл на сетевом томе всё равно может заблокироваться навсегда. И stat не заменяет change_token(path). Многие файловые системы хранят mtime с точностью до секунды, поэтому наблюдатель, построенный на mtime, может пропустить изменение. Чтобы заметить изменение, используйте change_token, а stat — когда нужно само число.
use std::fs;
test stat_reports_size_and_kind (mut sim: Sim) {
match fs::write_sync(&sim.fs, "a.bin", &b"abc") { Ok(_) => {} Err(_) => { assert false; } }
match sim.fs.stat("a.bin") {
Ok(fi) => { assert fi.size == 3u64; assert fi.kind == FileKind::Extent; }
Err(_) => { assert false; }
}
match sim.fs.mkdir("d") { Ok(_) => {} Err(_) => { assert false; } }
match sim.fs.stat("d") {
Ok(fi) => { assert fi.kind == FileKind::Dir; assert fi.size == 0u64; }
Err(_) => { assert false; }
}
match sim.fs.stat("never-written") {
Ok(_) => { assert false; }
Err((e, _)) => { assert e == FsErr::NotFound; }
}
}
// Сначала размер, потом байты. Тогда запись, вклинившаяся между ними,
// становится видна: числа расходятся, и вызывающий повторяет попытку. При
// обратном порядке записанный размер молча перестаёт описывать прочитанное.
pub fn sized_read[r](f: &Fs, path: &str, out: &r mut Vec[u8]) -> Result[u64, (FsErr, OsErr)] !{io, block, alloc(r)} {
let want = f.stat(path)?;
let got = fs::read_all(f, path, out)?;
if widen[u64](got) != want.size {
return Err((FsErr::Invalid, OsErr::Other));
}
Ok(widen[u64](got))
}
test the_sized_read_helper (mut sim: Sim) {
match fs::write_sync(&sim.fs, "r.bin", &b"payload!") { Ok(_) => {} Err(_) => { assert false; } }
region r {
let mut buf = alloc in r mut Vec::new();
match sized_read(&sim.fs, "r.bin", buf) {
Ok(n) => { assert n == 8u64; assert buf.len == 8usize; }
Err(_) => { assert false; }
}
}
}Атомарная замена как типовое состояние
Допустим, программа переписывает свой конфигурационный файл на месте, и посередине у машины пропадает питание. Теперь файл наполовину старый и наполовину новый, а это обычно хуже, чем любой из двух вариантов. Стандартное решение опирается на rename, который атомарно заменяет одно имя другим: любой, кто смотрит на путь, видит либо старый файл, либо новый, но не смесь. Полная последовательность такая: записать временный файл, сделать для него fsync, переименовать его поверх целевого и сделать fsync каталога, чтобы сохранилось и само переименование. Если пропустить первый fsync, после сбоя новое имя может указывать на файл, данные которого так и не дошли до диска. Последовательность хорошо известна, как и то, насколько легко ошибиться в одном из шагов. В kola она выражена тремя типами, и переставить шаги местами нельзя:
use std::fs;
test typestate_install (mut sim: Sim) {
region r {
let data = alloc in r [1u8, 2u8, 3u8];
let mut t = sim.fs.create_tmp("").unwrap();
let _w = t.write(data);
let s = t.seal().unwrap();
let _i = s.install("cfg.bin");
}
}create_tmp(dir) открывает в каталоге dir файл без имени. У Tmp есть write, write_at, set_mode, seal и abandon, а install нет. seal делает fsync файла, расходует Tmp и выдаёт Sealed, единственная операция которого — install(path). install выполняет переименование и fsync каталога одним шагом, так что сделать только половину вызывающий не может. Если пропустить seal, классическая ошибка «rename до fsync» становится ошибкой компиляции:
test typestate_skip_seal (mut sim: Sim) {
region r {
let data = alloc in r [1u8, 2u8, 3u8];
let mut t = sim.fs.create_tmp("").unwrap();
let _w = t.write(data);
let _i = t.install("cfg.bin"); // у Tmp нет `install`
}
}Компилятор сообщает о двух ошибках. Первая (card §5): у Tmp нет метода install. Вторая: ресурс t не израсходован на этом пути, ведь t всё ещё нужно как-то израсходовать.
Установленный так файл получает режим 0600, если до seal не вызвать t.set_mode(mode). Расширенные атрибуты (небольшие именованные значения, которые хранятся вместе с файлом, но вне его содержимого), заданные для Tmp до seal, сохраняются вместе с байтами и публикуются тем же переименованием, так что после сбоя файла либо нет, либо он есть вместе с метаданными. Для частого случая std::fs::install_bytes оборачивает всю последовательность:
pub fn install_bytes(fs: &Fs, dir: &str, path: &str, data: &[u8]) -> Result[(), (FsErr, OsErr)] !{io, block} {
let mut t = fs.create_tmp(dir)?;
match t.write(data) {
Ok(_) => {}
Err(e) => { t.abandon(); return Err(e); }
}
let s = t.seal()?;
s.install(path)
}Для перемещения существующих файлов есть rename(path, to): он переименовывает файл или каталог, а затем делает fsync обоих родительских каталогов. Между разными файловыми системами он завершается ошибкой FsErr::Invalid с OsErr::XDev, так что можно сопоставить Err((_, OsErr::XDev)) и перейти к копированию.
Пути отсчитываются от корня
Капабилити, дающая доступ к каталогу, не должна позволять программе добраться до файлов за его пределами. Выбраться наружу в пути позволяют две вещи: .., который поднимается в родительский каталог, и абсолютный путь, начинающийся с /. Сбежать может и символическая ссылка (symlink) — файл, в котором записан лишь путь к другому файлу. Корень sys.fs — рабочий каталог, и любой переданный путь разрешается внутри его корня. Литеральный путь с ведущим / или компонентом .. отвергается ещё при проверке, для корня любого вида:
pub fn load(fs: &Fs) -> Result[(), (FsErr, OsErr)] !{io, block} {
let f = fs.open_read("/etc/passwd")?;
f.close()
}error[card §7]: `open_read` is given an absolute path — a capability path is relative to its root
--> pathlit.kl:2:26
|
2 | let f = fs.open_read("/etc/passwd")?;
| ^^^^^^^^^^^^^Следующий за этим текст подсказки объясняет альтернативу: объявить выданный корень, принять его в программе, дать оператору его привязать и открыть путь внутри него (об этом следующий раздел). Проверка прослеживает литерал через функции-обёртки и через const, так что перенос строки в константу ошибку не вернёт.
Вычисленный путь (из конфигурационного файла, запроса или аргумента) проверить при компиляции нельзя, поэтому решение принимается во время выполнения и возвращается как значение, а не как трап. Два тега ошибки говорят, кто отказал:
FsErr::Unreachable— вердикт песочницы по самой строке и по вашим выдачам: любой..или абсолютный путь за пределами базы корня. Ни повтор, ниchmodего не изменят; исправлять нужно выдачу или написание пути.FsErr::Denied— вердикт файловой системы:EACCESили символическая ссылка, которая вывела бы за корень.
Проверяется каждый компонент, и символическая ссылка, выводящая за корень, отвергается, даже если у неё абсолютная цель, которая указывает обратно внутрь. Цель символической ссылки — данные, пришедшие от файловой системы, а подложить такую ссылку может любой, у кого есть право записи в каталог. С абсолютным путём, который передаёт сама программа, обходятся иначе: он принимается, если лежит внутри базы корня, ведь его набрал оператор. Для корня рабочего каталога база — физический рабочий каталог, и сравниваются строки, поэтому путь, записанный через каталог-символическую ссылку (например, /tmp в macOS), даёт Unreachable, а тот же файл, записанный физическим путём, принимается. С относительными путями такого вопроса не возникает. Под kola test у симулированной файловой системы нет рабочего каталога, так что вычисленный абсолютный путь там отвергается всегда.
subtree(prefix) ослабляет корень до подкаталога — тот же приём сужения, что и dir.ro(). У поддерева своей базы нет, поэтому абсолютные пути оно отвергает сразу, а литеральный префикс с / или .. отвергается при проверке:
fn escaping_path() -> &heap str !{} { "../secret" }
test escape_is_refused_at_runtime (mut sim: Sim) {
let sub = sim.fs.subtree("work");
match sub.open_read(escaping_path()) {
Ok(f) => { let _c = f.close(); assert false; }
Err((e, _)) => { assert e == FsErr::Unreachable; }
}
}Выданные корни
Демону обычно нужен каталог вроде /var/lib/thing, а рабочий каталог у него где-то в другом месте. Ослаблять ради этого песочницу kola не станет. Вместо этого именованные каталоги открываются на уровне корня, как это сделано в системном интерфейсе WebAssembly (WASI) и в песочнице Capsicum из FreeBSD: оператор привязывает имя к пути, программа запрашивает имя, а само имя записано в манифесте пакета, где его увидит рецензент.
sys.fs.root(b"db") запрашивает доступ на чтение и запись и отвечает Option[Dir]. sys.fs.root_ro(b"cfg") запрашивает доступ только на чтение и отвечает Option[DirRo]. Неизвестное имя или запрос на запись к корню, выданному только для чтения, дают None, а не ошибку. У Dir те же операции с путями, что и у Fs, но с корнем в выданном каталоге. У DirRo есть только читающие операции, поэтому запись через корень только для чтения — ошибка компиляции. d.ro() сужает Dir до DirRo.
use std::log;
fn main(sys: Sys) -> u8 !{io, block, alloc(heap), log} {
let d = match sys.fs.root_ro(b"cfg") {
Some(d) => d,
None => { log::error("no cfg grant"); return 2; }
};
let mut f = match d.open_read("app.conf") {
Ok(f) => f,
Err(_) => { log::error("open failed"); return 3; }
};
let mut buf = [0u8; 64];
let n = match f.read(&mut buf) { Ok(n) => n, Err(_) => 0usize };
let _c = f.close();
log::info(f"read {n} bytes through the grant");
0
}$ kola run roots.kl --fs-root cfg:ro=/etc/app --log stderr
[info] read 12 bytes through the grant--fs-root name=/abs выдаёт доступ на чтение и запись, name:ro=/abs — только на чтение. Двойник в окружении — KOLA_FS_ROOT_cfg (значение ro:/abs для доступа только на чтение), и скомпилированный бинарник его тоже читает. Вызов пишущей операции через дескриптор только для чтения отвергается при проверке:
error[card §7]: `DirRo` is a READ-ONLY granted root — `create` writes, which a read-only root cannot reachМанифест пакета объявляет каждое имя, которое программа может запросить, — [caps] fs_roots = ["cfg"], — и kola audit их перечисляет. Запись в манифесте может также назвать расположение по умолчанию из закрытого списка (xdg-config, xdg-data, xdg-state, xdg-cache, etc, root), которое используется, если оператор не задал переменную; путей манифест не пишет никогда.
Код, принимающий выданный корень, можно тестировать. Под kola test имя, перечисленное в fs_roots манифеста, становится симулированным корнем, и sim.fs.root(b"cfg") отвечает Some. Вне пакета передайте --sim-fs-root cfg:
use std::fs;
fn read_conf(d: &Dir) -> Result[usize, (FsErr, OsErr)] !{io, block} {
let mut f = d.open_read("app.conf")?;
let mut buf = [0u8; 64];
match f.read(&mut buf) {
Ok(n) => { f.close()?; Ok(n) }
Err(e) => { let _c = f.close(); Err(e) }
}
}
test reads_through_a_granted_root (mut sim: Sim) {
let d = sim.fs.root(b"cfg").unwrap();
let w = fs::write_sync_in(&d, "app.conf", &b"port = 8080");
assert w == Ok(());
assert read_conf(&d) == Ok(11usize);
}$ kola test --sim-fs-root cfg conf.klПриватные файлы, dup и FsNode
В Unix у каждого файла есть биты прав, его режим: кто может его читать и писать. Режим 0600 означает, что это может только владелец. create_private(path) — вариант create с режимом 0600 и флагом O_EXCL, из-за которого открытие завершается ошибкой, если файл уже существует. Файл недоступен другим пользователям с самого момента появления, а существующий путь отвергается, а не обрезается, так что подменить файл ключа через гонку не получится. Есть у Fs и Dir.
is_private(path) -> Result[bool, (FsErr, OsErr)] проверяет это обещание позже, когда чей-нибудь cp -r, восстановление из резервной копии или chmod -R уже могли его нарушить. Ok(true) означает «доказуемо приватен»: режим никому больше доступа не даёт, и никакой список управления доступом (ACL — дополнительный список прав для отдельных пользователей, который поддерживают некоторые файловые системы) не открывает доступ, которого не видно по режиму. (В macOS chmod +a "everyone allow read" оставляет режим 0600 и позволяет читать файл всем, поэтому проверка смотрит и на ACL.) Ok(false) означает «приватность не доказана», и продолжать работу не следует. Ничего этот вызов не исправляет. В симуляторе ответ зависит от того, как файл был создан.
Если у вас открытый дескриптор и нужны сведения о нём, f.security() сообщает владельца, действующего вызывающего, число ссылок, является ли объект обычным файлом, приватность и размер — для удерживаемого дескриптора, без повторного поиска по пути. В пару к нему подходит open_read_nofollow(path): он отвергает символическую ссылку в последнем компоненте, и его нельзя подвесить на FIFO.
f.dup() возвращает второй, независимый File со своей обязанностью закрытия (это системный вызов dup: второй дескриптор того же открытого файла). Потоки в kola не разделяют память, так что рабочий поток не может заимствовать дескриптор; линейный ресурс должен переместиться в поток, который им пользуется (см. Потоки и каналы). dup позволяет таблице дескрипторов сохранить свою запись, пока копия уходит в другой поток. Дескрипторы со счётчиком ссылок означали бы две живые ссылки на один ресурс, а линейность это запрещает.
FsNode указывает на объект файловой системы, а не на открытый файл (как дескриптор O_PATH в Linux, который удерживает файл, не открывая его ни на чтение, ни на запись). Он следует за объектом при переименовании и продолжает работать после удаления последней жёсткой ссылки. Его единственные операции — open_read() и close(), поэтому читать через него напрямую — ошибка компиляции. Чтобы его получить, на разных системах нужны разные права, так что переносимый код обрабатывает отказ и в open_node, и в open_read.
use std::fs;
test an_anchor_opens_readable_files (mut sim: Sim) {
match fs::write_sync(&sim.fs, "n.bin", &b"78") { Ok(_) => {} Err(_) => { assert false; } }
match sim.fs.open_node("n.bin") {
Ok(n) => {
// Открытие не расходует якорь: открываем через него дважды.
match n.open_read() {
Ok(f) => { let _c = f.close(); }
Err(_) => { assert false; }
}
match n.open_read() {
Ok(f2) => { let _c2 = f2.close(); }
Err(_) => { assert false; }
}
let _cn = n.close();
}
Err(_) => { assert false; }
}
}
test dup_yields_a_second_independent_handle (mut sim: Sim) {
match fs::write_sync(&sim.fs, "d.bin", &b"AB") { Ok(_) => {} Err(_) => { assert false; } }
let a = sim.fs.open_read("d.bin").unwrap();
let b = a.dup().unwrap();
let _ca = a.close(); // закрываем оригинал...
let mut buf = [0u8; 2];
let n = b.read_at(0u64, &mut buf).unwrap(); // ...а копия по-прежнему работает
let _cb = b.close(); // два дескриптора, две обязанности
assert n == 2usize;
assert buf[0] == 65u8;
}
test create_private_refuses_an_existing_path (mut sim: Sim) {
let f = sim.fs.create_private("id.key").unwrap();
let _c = f.close();
match sim.fs.create_private("id.key") {
Ok(f2) => { let _c2 = f2.close(); assert false; }
Err((e, _)) => { assert e == FsErr::Exists; }
}
}
test is_private_answers_how_the_file_was_created (mut sim: Sim) {
let f = sim.fs.create_private("vault.key").unwrap();
let _c = f.close();
match sim.fs.is_private("vault.key") { Ok(p) => { assert p; } Err(_) => { assert false; } }
let g = sim.fs.create("notes.txt").unwrap();
let _d = g.close();
match sim.fs.is_private("notes.txt") { Ok(p) => { assert !p; } Err(_) => { assert false; } }
}Блокировки
Блокировка позволяет взаимодействующим процессам пользоваться файлом по очереди, например чтобы два писателя никогда не перемешивали свои обновления. Файловые блокировки в Unix рекомендательные: они останавливают только те программы, которые тоже запрашивают блокировку, а программа, которая её не запрашивает, по-прежнему может читать и писать. Блокировка бывает разделяемой (много читателей одновременно) или исключительной (один владелец). У File два семейства рекомендательных блокировок. Блокировки всего файла — try_lock_shared, try_lock_exclusive и unlock (реализованы через flock). Блокировки диапазона — try_lock_range_shared, try_lock_range_exclusive и unlock_range (реализованы через fcntl-блокировки уровня открытого файлового описания); длина диапазона 0 означает «до конца файла». Каждый вызов try_ отвечает Ok(true), если блокировку удалось взять, и Ok(false), если нет.
Не смешивайте два семейства на одном файле. Исключают ли они друг друга, зависит от системы: блокировку диапазона, запрошенную, пока другой дескриптор держит весь файл, Linux выдаёт, а macOS отказывает. Программа, которая в одном месте блокирует весь файл, а в другом диапазон, получает взаимное исключение на одной системе и не получает на другой, и никто об этом не сообщит. Симулятор считает семейства независимыми, как Linux, поэтому тест, запущенный в macOS, скажет обратное тому, что там произойдёт на самом деле. Выбирайте одно семейство на файл.
hint_evict(off, len) просит платформу выбросить из кеша страницы диапазона и ничего не возвращает, потому что подсказка, сообщающая об успехе, была бы уже гарантией. Linux страницы выбрасывает, macOS не делает ничего. Если для корректности чтение обязано дойти до накопителя, откройте файл для прямого ввода-вывода (open_direct_read, open_direct_rw). Прямой ввод-вывод полностью обходит страничный кеш, но требует строгого выравнивания буферов и смещений.
Детерминированное тестирование сбоев
Под kola test sim.fs — это SimFs: та же поверхность File, Tmp и Sealed, но в памяти, и несколько вызовов только для тестов. arm_crash() и crash() останавливают симулированный процесс на границе сохранности и дают тесту посмотреть, что уцелело:
use std::fs;
test install_survives_crash (mut sim: Sim) {
region r {
let old = alloc in r [79u8, 76u8, 68u8]; // "OLD"
let new = alloc in r [78u8, 69u8, 87u8, 33u8]; // "NEW!"
let _w = fs::write_sync(&sim.fs, "cfg", old);
sim.fs.arm_crash();
match fs::install_bytes(&sim.fs, "", "cfg", new) { Ok(_) => {} Err(_) => {} }
sim.fs.crash();
let out: &r mut Vec[u8] = alloc in r mut Vec::new();
match fs::read_all(&sim.fs, "cfg", out) {
Ok(n) => { assert n == 3usize || n == 4usize; } // всё старое или всё новое, но не смесь
Err(_) => { assert false; } // и файл никогда не пропадает
}
}
}Проверка допускает 3 байта (весь старый файл) или 4 (весь новый) и ничего больше. Это и есть утверждение install о сохранности при сбое. Точка сбоя берётся из зерна теста, поэтому kola test --sim --seed N в точности повторяет один сбой, а прогон по многим зёрнам покрывает расписание. sim.fs.sim_capacity(bytes) задаёт размер симулированного диска: запись, которая его превысила бы, отвечает FsErr::NoSpace и ничего не меняет.
Один и тот же исходный текст работает в интерпретаторе (kola run), через C-бэкенд на настоящих файлах (kola run --native, kola build) и с симулятором (kola test).
Собственный бэкенд пока не поддерживает расширенные атрибуты; программа, которая ими пользуется, собирается через C. Прямой ввод-вывод есть в Linux, macOS и Windows x64, со своими для каждой системы правилами выравнивания, которые описаны в STD-CARD.