Skip to content

+ Partclone: read and write format 0002 images, verified by partclone - #414

Merged
Hawkynt merged 6 commits into
mainfrom
feat/partclone-write
Sep 30, 2026
Merged

Hawkynt merged 6 commits into
mainfrom
feat/partclone-write

Conversation

@Hawkynt

@Hawkynt Hawkynt commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

What changed

  • Read and write partclone image format 0002 in its real layout: 36-byte image_head, 52-byte file_system_info (superBlockUsedBlocks before usedblocks), 18-byte image_options, header CRC, bitmap plus bitmap CRC, CRC32 data strips (partclone's register without final inversion, reseed or not).
  • The reader verifies the header CRC, the bitmap CRC and every strip checksum (CRC32 and XXH64), refuses format 0001 and big-endian images, and accepts a byte map with either trailer.
  • The writer creates images from image.img (+ the metadata.ini and allocation.map an extraction produced) with CRC32 or no checksum and a bit map or no map. Byte maps are refused (partclone writes a CRC after them but reads them back expecting "BiTmAgIc", so it cannot restore them); XXH64 output is refused (no available partclone build could verify it).
  • List never throws on a damaged image; Create refuses foreign inputs with ArgumentException.

The reader on main modelled a layout that does not exist (31/51/22-byte structures, wrong checksum mode numbers) and could not read any image partclone writes; this branch replaces it.

Verification

  • Reference images written by partclone 0.3.27 (FAT12 source; CRC32 strip 2048, no checksum, strip 3 without reseed) are committed as gzip vectors: header fields, reconstruction, and byte-identical re-creation from the extracted entries.
  • New ExternalInterop fixture: partclone.chkimg accepts our images and partclone.restore reproduces the partition (strips of 1/7/256 blocks, no reseed, no checksum, no map); chkimg rejects a damaged one. Green locally against partclone 0.3.27 in WSL.

@Hawkynt
Hawkynt force-pushed the feat/partclone-write branch from efd9023 to 1a07e08 Compare September 30, 2026 12:25
@Hawkynt Hawkynt changed the title Write Partclone v2 images + Partclone: read and write format 0002 images, verified by partclone Sep 30, 2026
@Hawkynt
Hawkynt force-pushed the feat/partclone-write branch 4 times, most recently from 977a8fb to 118f61f Compare September 30, 2026 20:05
Partclone containers could be inspected and reconstructed but not created, so the archive descriptor exposed no write capability. Add a v2 writer that consumes the extracted raw image, metadata and allocation map, preserving allocated zero blocks and emitting supported CRC32 or XXH64 strips. Retain filesystem geometry and checksum parameters in metadata and add round-trip coverage.
Changing bitmap encoding previously rejected the original map and checksum-mode overrides retained the source mode's parameters. Translate maps between packed-bit and byte-per-block forms and select compatible size/strip defaults whenever the checksum mode changes.
Partclone's BM_NONE mode represents every block as present without a bitmap. The writer previously treated that legal mode as unsupported; emit all blocks and preserve the same semantics when converting to bitmap-backed modes.
Symptom: CI did not compile (CS0173 in PartcloneFormatDescriptor.Create).
Past that, none of the partclone code could read an image partclone
writes, and partclone could not read what the new writer produced. The
tests passed only because their synthetic builder shared the reader's
mistakes.

Root cause: the reader, the writer and the test builder all modelled a
layout that does not exist:
- image_head is 36 bytes (16-byte NUL-terminated magic, 14-byte version,
  "0002", endianess), not 31; file_system_info is 52 bytes with a 16-byte
  fs field and superBlockUsedBlocks BEFORE usedblocks; image_options is
  18 bytes and is followed by a separate header CRC (110 bytes in all).
- The header CRC was never computed or checked.
- On-disk checksum modes are 0x20 (CRC32) / 0x30 (XXH64), not the 1/2
  that partclone's -a option and IMAGE_FORMATS.md show; BM_BYTE is 0x08.
- The bitmap is always followed by a 4-byte CRC (whenever a bitmap
  exists), not by checksum_size bytes only when data checksums are on.
- partclone's CRC32 is the register without the final inversion.

Fix:
- PartcloneReader parses the real descriptor, verifies the header CRC,
  the bitmap CRC and every strip checksum (CRC32 and XXH64, reseed and
  no-reseed), refuses format 0001 and big-endian images with
  NotSupportedException, and accepts a byte map with either trailer.
- PartcloneWriter writes the same layout: CRC32 or no checksum, bit map or
  no map, partclone's default strip (1 MiB / block size). Byte maps are
  refused because partclone writes a CRC after them but reads them back
  expecting "BiTmAgIc", so it cannot restore such an image; XXH64 output
  is refused because no available partclone build could verify it.
- Create refuses anything but image.img (+ metadata.ini, allocation.map)
  with ArgumentException; List no longer throws on a damaged image and
  lists a metadata.ini with parse_status = partial instead.
- Tests are rebuilt on reference images from partclone 0.3.27: header
  fields, reconstruction, byte-identical re-creation from the extracted
  entries, and one equivalence class per kind of damage.
- New ExternalInterop fixture: partclone.chkimg accepts our images and
  partclone.restore reproduces the partition (CRC32 strips of 1/7/256
  blocks, no reseed, no checksum, no map), and chkimg rejects a damaged
  one.

Sourcing: rung 2 then 3. partclone is GPL-2.0, so its IMAGE_FORMATS.md
and struct definitions were read as a specification only and nothing was
carried over; where the document disagrees with partclone's own output
(checksum mode numbers), the images partclone 0.3.27 wrote are the
reference. Those images are committed as gzip vectors, and partclone
0.3.27 is the oracle for the external fixture.
@Hawkynt
Hawkynt force-pushed the feat/partclone-write branch from 118f61f to 7542269 Compare September 30, 2026 20:59
@Hawkynt
Hawkynt merged commit 1947641 into main Sep 30, 2026
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant