У него есть прошлое
На старте есть изображение `kot2_proshloe.png`, в котором записан образ диска с CatFS версии 2. Образ содержит снимок каталога, журнал транзакций и сектора данных. Нужно восстановить последнюю зафиксированную версию `nose_note.txt` перед её удалением.
## Решение
### Извлечение CatFS
В PNG находим `iTXt`-запись `CatFS.boot`. Она описывает геометрию носителя: плитки 32 × 32 от `(0, 0)`, обход по строкам, один бит в чётности синего канала (`B & 1`). Байты собираются старшим битом вперёд.
В плитке `32 × 32 = 1024` бита, то есть один сектор на 128 байт. Для изображения 1254 × 1254 получаем `39 × 39 = 1521` полный сектор.
Сектор `0` начинается с `CATF`, версия — `2`. Сверяем CRC32 первых 124 байт со значением по смещению `124` и читаем карту областей:
| Область | Сектора |
|---|---|
| Суперблок | `0` |
| Снимок каталога | `1–2` |
| Журнал транзакций | `3–7` |
| Данные | С сектора `8` |
В суперблоке также указано `sectors_used=107` — это число занятых секторов всей файловой системы, включая служебные области.
У сектора данных есть `sector_id`, `next_sector`, `used_length` и `payload_crc`. Заголовок сектора занимает 16 байт, а CRC покрывает всё следующее 112-байтное поле payload, включая неиспользованную добивку. Следуем по `next_sector` до `0xffffffff`, полезную длину учитываем при сборке файла.
Запись каталога занимает 32 байта: `name[16]`, `inode u32`, `generation u16`, `reserved u8`, `flags u8`, `size u32`, `first_sector u32`. По снимку читаем живой `format.txt` — там правила журнала и точная цель восстановления.
### Снимок записки
В снимке `nose_note.txt` имеет `inode=1006`, `generation=1`, `first_sector=100`. У записи выставлен бит сжатия `flags & 2`. Собираем полезные байты цепочки секторов с номерами от `100` до `106`, затем распаковываем zlib. Получаем 1504 байта текста, но флаг обрывается на `caplag{zhurn4l_p0m`. Хвост появился позже, в журнале.
Записи журнала имеют формат `[type u8][reserved u8][length u16][payload][crc32]`. Многобайтные числа читаем в little-endian. CRC32 покрывает четырёхбайтный заголовок записи и её payload. Читаем до `type=0`, сверяем CRC и группируем операции между `TXN_BEGIN` и соответствующим `TXN_COMMIT`.
### Применение журнала
Сначала загружаем живые объекты снимка, сохраняя для каждого inode поколение и логические байты файла. Затем читаем транзакции в порядке журнала и применяем только завершённые COMMIT. Незавершённую транзакцию отбрасываем при следующем BEGIN или конце журнала.
| Транзакция | Действие | Что делаем |
|---|---|---|
| `t2`, есть COMMIT | `OP_WRITE` к `(1006, gen 1)`, offset `1504`, replace_len `0` | Распаковываем zlib-патч, дописываем его байты и получаем gen 2 |
| `t3`, нет COMMIT | `OP_WRITE` к `(1006, gen 2)`, offset `1486`, replace_len `31` | Отбрасываем всю транзакцию |
| `t4`, есть COMMIT | `OP_DELETE` к `(1006, gen 2)` | Сохраняем последнюю версию и фиксируем удаление |
| `t5`, есть COMMIT | `OP_WRITE` к `(1006, gen 0)` | Создаём новую серию inode для `draft.txt` |
У `t3` правильные CRC отдельных записей, но это не делает транзакцию зафиксированной. Если применить её без COMMIT, настоящий текст заменится ложным.
Одного номера inode недостаточно, поскольку операции относятся к паре `(inode, generation)`, а создание с `generation=0` после удаления начинает новую серию. Поэтому `draft.txt` из `t5` не заменяет искомую историю записки.
Патч `t2` дописывает недостающий хвост. Последняя зафиксированная версия перед `t4` — gen 2 длиной 1568 байт. В её конце читаем полный флаг.
[Солвер](solve/solve.py).
## Флаг
`caplag{zhurn4l_p0mn1t_k0t4_n0s}`