@kawaki-dev/kwq
v0.2.0
Published
Формат паков Kawaki Quiz (.kwq): схемы, чтение, запись, валидация
Readme
@kawaki-dev/kwq
Формат паков Kawaki Quiz (.kwq): схемы, чтение, запись, валидация.
Пакет ничего не знает ни о файловой системе, ни о сети — на входе и на выходе байты. Поэтому один и тот же код работает в редакторе (Tauri), в браузере и в серверном роуте загрузки, и валидация у них заведомо одна и та же.
Контейнер (zip, имена медиа по sha256, пределы, защита от zip-бомбы, типы
медиа) живёт в @kawaki-dev/pack и ставится как зависимость; здесь
только то, что относится к викторине: доска, вопросы, носители, их проверка.
Публичный API и формат файла от этого не менялись.
Формат
my_quiz.kwq zip
├── manifest.json описание пака и опись медиа
├── pack.json доска: раунды, темы, вопросы, ответы
└── media/
├── 3f9a…c1.jpg имя файла = sha256 содержимого
└── 8b12…7e.mp3Имя медиафайла — его контрольная сумма, и допустимое имя записи описывается
одной регуляркой ^media/[0-9a-f]{64}\.[a-z0-9]{1,4}$. Отсюда главное
свойство контейнера: path traversal исчезает как класс, а не отлавливается
проверками на .. и абсолютные пути. Заодно пропадают дубли, кириллица в
именах и вопрос «а тому ли файлу соответствует этот хеш».
Пак — данные. Ничего исполняемого в нём нет: белый список типов не содержит ни SVG, ни HTML, а содержимое проверяется по сигнатуре, а не по расширению.
Использование
import { writePack, validatePack, readPack, mediaIdOf } from "@kawaki-dev/kwq";
// Сборка. Идентификатор медиа считается при импорте файла в редактор.
const bytes = await file.arrayBuffer().then((b) => new Uint8Array(b));
const { bytes: archive } = await writePack({
title: "Аниме на вечер #1",
media: [{ id: await mediaIdOf(bytes), bytes, kind: "image", mime: "image/jpeg" }],
board: { rounds: [/* … */] },
});
// Проверка: список проблем, а не исключение на первой же.
const result = await validatePack(archive);
if (!result.ok) {
for (const issue of result.issues) console.log(issue.code, issue.path, issue.message);
}
// Чтение, когда нужен пак или отказ.
const pack = await readPack(archive); // бросает KwqError со списком проблемwritePack заканчивает тем, что читает собранное собственным валидатором:
сломанный редактор не сможет молча выпустить пак, который сервер отвергнет уже
после заливки.
Сервер обязан звать validatePack заново на всё, что прилетело от
клиента: .kwq может быть собран не нашим приложением.
Расширяемость
Тип вопроса — запись в реестре: схема params, подпись для
редактора и требуемая механика. Пак объявляет нужные механики в
manifest.requires, поэтому новые типы вопросов не поднимают
formatVersion: читатель, не умеющий механику, отвечает «паку нужен
аукцион, обновите приложение», а не «пак повреждён».
Версия формата растёт только на несовместимой перестройке структуры.
Разработка
npm test # tsc --noEmit и тесты, включая порчу архива и чтение sample.kwq
npm run build # нужен собранный @kawaki-dev/pack: из корня `npm run kwq:build`
npm run sample # пересобрать fixtures/sample.kwqfixtures/sample.kwq лежит в репозитории готовым: он и пример формата, и то,
на чём проверяются читатели с другой стороны.
