MF-WE — движок записи (ТЗ MF-WE-TZ-001)¶
mountfile/write_engine.py — транзакционный фасад над RW-бэкендами
mountfile/universal_fs_rw.py (native FAT32 и ограниченные clean ext2/exFAT
profile; явный library-level legacy ext2/3/4 compatibility backend через
debugfs; NTFS через системный/mount-helper backend). Native ext2 — первая фаза
собственной записи; GUI и WriteEngine не переключают ext3/ext4 автоматически
на внешний writer до отдельной journal/recovery-aware реализации (issue #182).
Новый NTFS boot-sector probe и FILE-record parser ниже только проверяют
метаданные и не переключают существующий backend, не предоставляют полное
чтение файлов и не разрешают запись.
Что гарантирует MF-WE¶
| Требование ТЗ §8 | Реализация |
|---|---|
| RO по умолчанию | WriteEngine(image) — все мутации бросают ReadOnlyError; запись только при read_only=False |
| Backup перед записью | снапшот сессии + снапшот транзакции (ротация keep_snapshots, по умолчанию 3) |
| Транзакции с журналом | EngineJournal: JSONL с полями prev/hash; verify_journal() пересчитывает цепочку |
| Verify после commit | verify(): размер образа, структурные проверки BPB/суперблока; ext2 writer проверяет staged metadata после каждой мутации, независимый e2fsck для него не запускается |
| Откат | rollback() возвращает образ к состоянию на старте транзакции побайтово; restore_from_backup() — к снапшоту |
| Dry-run | dry_run=True: проверяются пути и предусловия, ни один байт образа не меняется |
| Запрет записи в повреждённые образы | damage_report() + DamagedImageError; обход только явным allow_damaged=True |
Проверки, которым нужен внешний инструмент, при его отсутствии возвращают
ok=None (неизвестно), а не «прошло» — verify() не выдаёт непроверенное
за проверенное.
Native exFAT read-only profile¶
VFS can browse a validated exFAT image without pytsk3 when an explicit
partition extent is supplied. The bounded provider supports directory listing,
file sizes, bounded range reads and extraction through the common read API. It
accepts validated one-FAT profiles only. Two-FAT TexFAT volumes are refused for
native browsing and writes regardless of the selected active FAT or bitmap
contents. The boot-region probe is geometric, not an access authorization.
Dirty or media-failure volumes are rejected; ClearToZero is allowed only for
reading. A range read fetches only the clusters and byte spans intersecting
the requested range. The provider
validates the main and backup boot regions, cluster chains and checksummed file
entry sets. Opening the provider validates the allocation bitmap and the
checksum-protected, full-range $UpCase table (compressed or uncompressed).
The same volume-specific up-case mapping is used for NameHash verification,
path lookup and writer-created or renamed entries. The allocation bitmap is
authoritative: clusters traversed in FAT chains and NoFatChain contiguous
streams must be marked allocated, while FAT entries for NoFatChain streams
are ignored as required by exFAT. The reader never writes the image. Mutations
are provided separately through the explicitly bounded NativeExfatRW writer
described below.
Bounded extraction¶
mountfile.extract.extract_path() and extract_file() apply ExtractionLimits
by default: 1 GiB per file, 4 GiB total for a tree, 100,000 regular files,
200,000 directory entries, and 64 nested directory levels. Callers may pass a
stricter policy explicitly:
from mountfile.extract import ExtractionLimits, extract_path
extract_path(
provider,
"/evidence",
"./out",
limits=ExtractionLimits(
max_file_size=256 * 1024 * 1024,
max_total_size=1024 * 1024 * 1024,
max_files=10_000,
max_entries=20_000,
max_depth=32,
),
)
Providers with file-size and range-read APIs are rejected before reading an oversized file and copied in bounded chunks. Older whole-file providers retain compatibility; their returned payload is checked before it is written, but may already have been buffered in memory. Limits do not provide tree-wide rollback: files completed before a later limit/error remain in the output directory. Host-side extraction limits also do not add filesystem-image write capability.
Native exFAT phase-one writes¶
UniversalFSRW exposes native-exfat-phase1 only for an explicitly bounded
volume with exactly one FAT. Two-FAT TexFAT is rejected before mutation, with
no legacy fallback after a native profile refusal; mirrored FAT writes are not
a TexFAT transaction or recovery protocol. Dirty, media-failure, ClearToZero
and reserved VolumeFlags are rejected before mutation. It supports file
creation and replacement, directory creation and growth, same- and
cross-directory rename, and file or recursive directory
deletion. Before freeing clusters it scans allocation ownership and rejects
cross-linked or inconsistent streams. Each operation copies the complete image
to a staging file, validates the staged image through the native reader, checks
the full source-image size and SHA-256 under the native exFAT image lock, and
commits with an atomic replacement only if that identity is unchanged. This
detects changes both inside and outside the selected exFAT extent during
staging, preserving concurrent external changes instead of replacing them with
an older staged copy. The lock is advisory; an unrelated writer that ignores it
can still race in the final identity-check/replacement window. This is not a
power-loss recovery protocol. A rejected commit, short write or failed read-back
discards the stage; unsupported or ambiguous metadata still fails closed.
The Linux integration suite also formats a disposable image with mkfs.exfat,
then exercises replacement after shrinking, nested-file creation, cross-directory
Unicode rename, empty-directory removal and a zero-length file. It checks
mixed-case lookup and range reads, then runs fsck.exfat -n on a separate copy
and verifies that the checker accepts the image without changing that copy. A
second real image uses 512-byte clusters to force root-directory growth and is
checked through the same independent oracle. Linux CI installs exfatprogs;
other platforms keep the synthetic exFAT tests and report the Linux-only
real-image checks as explicit skips.
Использование¶
from mountfile.write_engine import WriteEngine
engine = WriteEngine("dump.img", read_only=False)
with engine.transaction("добавить заметку"):
engine.mkdir("/EVIDENCE")
engine.write_file("/EVIDENCE/NOTES.TXT", b"collected")
engine.rename("/EVIDENCE/NOTES.TXT", "/EVIDENCE/FINAL.TXT")
report = engine.verify()
assert report["ok"], report["problems"]
engine.close()
Любое исключение внутри with engine.transaction(...) откатывает транзакцию
целиком. Одиночный вызов без явной транзакции оборачивается в неё автоматически.
Границы¶
- FAT12/16/32 — bounded pure-Python readers
native-fat12-ro,native-fat16-roиnative-fat32-roвыбираются через unified VFS безpytsk3. Они проверяют BPB-геометрию, FAT12 packed entries, цепочки кластеров и LFN checksum. Bounded writers доступны для FAT12/16 черезFat1216RWи для FAT32 черезFat32RWвmountfile/universal_fs_rw.py. FAT12/16 writer требует fixed root directory; эта ограниченная запись не означает полного write parity. Native reader отклоняет рассинхронные mirrored FAT copies и повреждённую backup BPB FAT32 до чтения файлов. Статус read parity (issue #182): подтверждена differential тест-матрицей вtests/test_fat_reader.py— для всех трёх вариантов FAT прогоняются valid- и invalid-fixture (синтетические образы со short-name, LFN, пустым, многокластерным файлом и вложенным каталогом), а результаты list/stat/read-range/extract native reader сравниваются с отдельной минимальной reference-читалкой, не разделяющей код сmountfile.fat_reader. Дополнительно есть real-image differential test противpytsk3, использующийmkfs.vfatи mtools; integration CI устанавливает эти инструменты. Synthetic reference и real-image oracle не доказывают полную format parity. Повреждённые профили (битая boot signature, усечённый образ, цикл или свободная ссылка в цепочке кластеров) завершаются явным отказом; routing-тесты дополнительно фиксируют, что bounded FAT-профили всегда обрабатываются native reader-ом и legacypytsk3fallback не вызывается. Статус write parity: bounded native write path есть для FAT12/16 и FAT32, но полная WRITE parity не заявляется ни для одного из них. FAT12/16 ограничены фиксированным root directory; FAT32 writer остаётся bounded-движком. Writer принимает только mirrored FAT32 mode с идентичными FAT copies. Non-mirrored mode и рассинхронные копии отклоняются до mutation, потому что текущий writer обновляет все FAT copies одновременно и не поддерживает active-FAT semantics. В этом профиле rename поддерживает перенос файлов и каталогов между существующими каталогами: LFN-записи строятся заново с checksum нового short-name alias, для перенесённого каталога обновляется.., а перенос в себя/потомка, collision, malformed LFN и отсутствие свободных directory slots отклоняются до commit. Same-directory LFN rename и расширение каталога остаются неподдерживаемыми; FAT32 active-FAT semantics не включены. Перенос проходит на staged image с read-back, но это не on-volume recovery protocol и не доказательство power-loss atomicity. Если BPB объявляет backup boot sector, он должен быть байт-в-байт идентичен primary boot sector; повреждённая резервная BPB отклоняется до записи. BPB_FSInfo принимается только как0,0xffffили сектор внутри reserved region, отличный от backup boot sector; указатель в data area или alias резервной загрузочной записи отклоняется при открытии writer-а до mutation. -
Чистый ext2 с allowlisted features (1/2/4 KiB blocks, без журнала) использует собственный Python writer без
debugfs. Regular files поддерживают bounded direct, single- и double-indirect addressing до 64 MiB; sparse и triple-indirect файлы отвергаются. Каталоги поддерживают bounded direct, single- и double-indirect growth до 64 MiB; pointer blocks, bitmap/counters и directory map проходят read-back до commit. Unsupported/dirty/out-of-bounds volumes не получают native RW. Перед изменением image writer сохраняет и fsync-ит рядом с образом проверенные before- и staged after-image выбранного ext2 extent, затем атомарно публикует<image>.mountfile-ext2-recovery.jsonmarker формата 3 и fsync-ит родительский каталог. При восстановлении сверяются identity image и SHA-256 обеих копий; rollback разрешён только когда каждый 512-байтовый сектор совпадает с одним из двух сохранённых состояний. Неизвестные/повреждённые данные, внешнее изменение или legacy commit-marker без after-image оставляют sidecars на месте и блокируют дальнейшую запись вместо безусловной перезаписи. Snapshot открываются с проверкой типа/размера файла и удерживаются через один и тот же file descriptor до окончания проверки и восстановления. Ошибка очистки sidecar после подтверждённого commit не запускает rollback. Подтверждённый rollback охватывает только выбранный extent, проверяется read-back, затем marker удаляется до sidecars. Process-crash tests покрывают прерывание commit, повторного recovery и завершённый commit до удаления marker. При обычном отказе rollback marker дополнительно привязывается к точному post-failure extent и mtime; любое последующее изменение блокирует автоматическое восстановление. Не редактируйте image и не удаляйте sidecar-файлы, пока recovery не завершён. Это не заменяет backup и не является универсальной power-loss гарантией для всех файловых систем, устройств и ОС. Межпроцессный lock advisory: редактор, который его игнорирует, всё ещё может изменить image одновременно с commit/recovery; проверки read-back обнаруживают многие такие изменения, но не устраняют все race conditions. directory fsync применяется там, где ОС его поддерживает. Для ext2 вне этого профиля и ext3/ext4 автоматического fallback нет: legacydebugfs -wдоступен только прямому library caller сallow_legacy_debugfs=True, с явнымvolume_sizeдля каждого nonzero offset. Он staging-копирует и commit-ит ровно filesystem extent;chmod/chown/symlinkостаются за этим compatibility API. -
NTFS — native bounded reader is available through the VFS, but native NTFS writes are not implemented. The
UniversalFSRWfacade refuses NTFS writes by default; the oldntfs-3g/ImDisk compatibility backend is reachable only with the explicitallow_legacy_mount=Trueoption. This compatibility path is not the issue #182 native writer and retains its driver/privilege requirements. -
exFAT — ограниченный native writer описан выше. ImDisk и системный Windows filesystem driver или Linux
LoopMountRWс драйвером — отдельные explicit compatibility paths, не расширяющие поддержку native writer. HFS+ и другие неподдерживаемые Windows filesystem drivers остаются read-only. Имена детектора (FAT16,FAT32,HFS+) нормализуются для mount-helper. - ISO9660, SquashFS, JFFS2 и UBIFS объявлены
read-only-format: чтение и извлечение доступны, но мутации отклоняются до монтирования. Эти форматы нельзя честно сделать обычными RW-ФС без пересборки образа по их спецификации. Fat32RWreceives and enforces the selected volume extent. It rejects a BPB whose declared size exceeds that extent and checks every relative read/write against the declared filesystem boundary. Partition-table extents are used when available; signature-scan hits (including FAT backup boot sectors) are never treated as partition ends. At nonzero offsets without an authoritative partition-table size, a caller must supplyUniversalFSRW(..., volume_size=...).UniversalFSRWis default-deny for unsupported profiles. System/mount-based fallback is disabled unless a caller explicitly opts intoallow_legacy_mount=True; a recognized filesystem declined by its native provider is never silently redirected to a different writer.UniversalFSRW.capability_status()andFSInfo.write_capabilitiesreport the exact bounded mutations of the selected backend. Native ext3/ext4 and NTFS writes fail with an explicit unsupported-profile reason; they do not invoke a subprocess or fall back to a legacy writer. A read-only backend reports an empty write capability list and a human-readable rejection reason. Capability/status schema v5 also publishes arecoverycontract. It records tested failure rollback and the ext2 recovery-sidecar process-kill evidence, while FAT/exFAT process-kill and quarantine remainnot_proven; all native writers reportpower_loss: unsupported. This is an explicit refusal of unstated durability claims, not a promise of filesystem-wide crash safety.- Если backend, драйвер или права отсутствуют, операция завершается явной ошибкой; фасад не сообщает об успешной записи после молчаливого RO-mount.
- Вне области действия: физические устройства, таблицы разделов, шифрованные тома, спарс-файлы/сжатие/EFS.
Ext-профиль и native ext2 read-only VFS¶
mountfile.vfs.probe_ext_profile(image_path, offset=0, volume_size=None) —
ограниченный pure-Python probe первичного 1024-байтового superblock ext2/3/4.
Он возвращает feature-derived классификацию, геометрию и флаги состояния,
наличия журнала и необходимости recovery. Для деталей feature flags и
superblock state см. Linux ext4 superblock specification
и JBD2 journal documentation.
Сам probe не является файловым reader: он не обходит каталоги и не читает
содержимое файлов. Отдельный mountfile.vfs.open_native_ext2(image_path,
offset=0, volume_size=...) открывает pure-Python NativeExt2Image для явного
чтения ограниченного ext2 subset. Provider проверяет границы раздела, group
descriptors, inode/block allocation bitmaps, linear directory records и
direct/single/double/triple-indirect block maps. Он требует явный volume_size
и открывает образ только для чтения. Один вызов чтения ограничен 16 MiB;
экспорт файлов использует 1 MiB ranged chunks, чтобы не собирать большие файлы
целиком в памяти.
Native reader принимает только clean ext2 revision 0/1 с 1/2/4 KiB blocks и
allowlisted features. Dirty/recovery-required volumes, JBD2/ext3, ext4 layout
features, unknown feature flags, indexed directories, compressed/encrypted/
extent/inline inodes и symlink traversal отклоняются. open_image_provider
автоматически выбирает этот read-only backend в GUI, CLI и MCP только при наличии
точного размера раздела (или whole-image ext2 без таблицы разделов); остальные
профили не становятся автоматически читаемыми через этот provider. Распознанный,
но неподдержанный ext profile отвергается; compatibility fallback на pytsk3
возможен только для нераспознанного формата и при явном opt-in. Это не разрешение
на запись и не FUSE export. phase1_ext2_profile_candidate по-прежнему означает
только предварительное соответствие write profile. Writer обязан самостоятельно
проверять затрагиваемые группы, bitmaps, inode metadata и partition bounds.
Ограниченный NativeExt3Image проверяет объявленный в superblock номер internal
JBD2 journal inode и читает именно этот inode; external journal devices остаются
запрещены. Активный журнал по умолчанию отклоняется. Только явный
replay_journal=True включает read-only overlay для поддерживаемого классического
descriptor/data/commit формата; после публикации overlay metadata-кэши очищаются,
чтобы дальнейшие reads видели согласованное представление. Это ограниченная
проверка/просмотр, не полноценное JBD2 recovery и не разрешение на запись.
Descriptor targets, указывающие на data-блоки самого journal inode, отвергаются
как при clean scan, так и при явном read-only replay.
Ограничения и риски¶
- Native FAT mutations enforce the selected extent and use a same-directory full-image staging copy, sidecar writer lock, source-hash guard, independent read-back, atomic replace, and rollback copy. A failed rollback quarantines the image and retains the rollback file for manual recovery. Staging requires free space for two additional image copies. The commit is atomic at file replacement, not a guarantee against storage hardware failure; use a disposable working copy, never an original evidence image. Native ext2 writes remain limited to the clean allowlisted profile and are not power-loss-atomic; ext3/ext4 and unsupported ext2 profiles remain read-only. Native NTFS metadata browsing requires its bounded boot/MFT/USA, volume-state, restart-area and directory-index validations. It remains read-only: log replay, attribute-list streams, unsupported runlists and NTFS mutations are not enabled.
GUI и WriteEngine(read_only=False) используют тот же UniversalFSRW,
поэтому это не только read/extract режим.
* Полный набор fuzz/crash-тестов из §9.1 ТЗ (Atheris/hypothesis, ≥10 000
итераций, kill -9 на каждом шаге) не выполнен — реализованы
детерминированные crash/rollback-сценарии в tests/test_write_engine.py.
* Покрытие строк/ветвей из §9.3 (≥85% / ≥75%, транзакционный слой ≥95%) на
текущем прогоне не подтверждено для всего пакета.
NTFS bounded native read profile¶
mountfile.vfs.probe_ntfs_profile(image_path, offset, volume_size) вызывает
pure-Python probe первичного 512-байтового NTFS boot sector. Он требует явную
внешнюю границу образа/раздела и проверяет OEM/signature, bytes-per-sector,
sectors-per-cluster, объявленный размер тома, расположение $MFT/$MFTMirr и
кодирование размеров MFT/index records. Автопоиск томов использует тот же parser,
чтобы отвергать NTFS-подобные случайные boot sectors и геометрию вне образа.
Разметка полей и кодирование размеров сверены с upstream Linux NTFS3 структурами
on-disk
и проверками геометрии.
mountfile.vfs.probe_ntfs_profile remains a geometry-only probe. The native
read-only provider open_native_ntfs_metadata(image_path, offset, volume_size)
requires the exact selected volume extent and validates the primary and backup
boot sectors, $MFT/$MFTMirr, FILE-record USA fixups, bounded attribute streams
and runlists, $Volume dirty flags, and clean $LogFile restart pages before
exposing a tree. It supports list/stat/read-range/extract through the existing
VFS; it does not replay the journal or authorize writes.
Within each FILE record, the 0xffffffff attribute terminator must end exactly
at bytes_in_use; even zero-filled trailing bytes are rejected.
STANDARD_INFORMATION supplies created, modified, ctime (MFT metadata
change), and accessed as UTC FILETIME strings in both list and stat results;
the compatibility date field is the same value as modified, including for
the volume root.
For nonresident attributes the reader retains and checks the on-disk allocated,
data and initialized sizes. Bounded reads expose zero bytes after initialized_size
up to data_size, without returning stale physical bytes from the allocated run.
The reader validates resident and nonresident metadata records, parent/sequence
references and bounded $I30 index roots/allocation blocks. It supports valid
Unicode names for listing. If MFT record 10 exposes the exact-size, bounded
unnamed $UpCase DATA stream, the reader validates and uses its 65,536-entry
UTF-16 mapping, including fixed-point and ASCII invariants, for case-insensitive
path lookup; otherwise Unicode lookup is exact-case only. Resident
$ATTRIBUTE_LIST values are now decoded with bounded entry lengths, 8-byte
alignment, UTF-16 names and MFT references. Type order, ASCII name/VCN order and
duplicate keys are checked; Unicode collation uses a validated $UpCase table
when available and is not guessed when absent. Bounded ATTRIBUTE_LIST resolution
supports validated extension-record references and contiguous nonresident extents;
nested extension lists, VCN gaps/overlaps and conflicting stream sizes fail closed.
The entry layout follows Microsoft's
ATTRIBUTE_LIST_ENTRY definition
and the Linux NTFS on-disk reference.
The reader also rejects malformed indexes, cycles, sparse/compressed/encrypted
data and invalid path components. Native NTFS writes remain disabled; the
system-driver path is a separate explicit compatibility choice, not a substitute
for $LogFile-aware native transactions.