Skip to main content

Pure-Python reader and writer for Linux romfs (-rom1fs-) filesystem images.

Project description

romfs

PyPI CI Python License

Pure-Python reader and writer for Linux romfs (-rom1fs-) filesystem images. Standard library only, no runtime dependencies.

romfs lets you build, inspect, and unpack the small read-only filesystem images the Linux kernel uses for initrds and embedded root filesystems. Images produced by the writer are byte-compatible with mount -t romfs and interchangeable with genromfs; the reader parses any such image, including real genromfs output with ./.. self-links and dedup hardlinks.

Features

  • Read an image into a node tree; list entries and read file contents lazily via mmap — large images don't blow up memory.
  • Write an image from a local directory tree, preserving the structure, regular files, and symbolic links.
  • CLI with list, unpack, pack, and diff subcommands; a bare romfs <image> prints header info and the per-entry space breakdown.
  • Importable library API for embedding in other tools.
  • Byte-exact conformance with the kernel romfs format: 16-byte alignment, big-endian, superblock + header checksums.
  • Entries sorted by name for a deterministic, diff-friendly layout.

[!NOTE] The writer lays out entries sorted by name, so two versions of the same resource tree produce byte-stable images — keeping binary diffs (delta packages) small.

Installation

pip install romfs

Run from source with no install:

./main.py <args>

Quick start

# Pack a folder tree into a single romfs image
romfs pack ./rootfs -o rootfs.img -n myvol

# Print header info + per-entry space breakdown for an image
# (an image path with no subcommand is shorthand for this)
romfs rootfs.img
romfs rootfs.img --verify     # also verify the superblock checksum

# List the file tree (type, size, path)
romfs list rootfs.img

# Unpack one file to stdout, or everything to a directory
romfs unpack rootfs.img etc/init.d/rcS
romfs unpack rootfs.img -o ./out

# Compare two images file-by-file (exit 0 if identical, 1 if different)
romfs diff old.img new.img
romfs diff old.img new.img --text   # also print a line-level diff for changed text files

diff output is a five-column table — symbol | old file | new file | size delta | detail — framed by dash rules. Columns 2/3 hold the path on each side (blank = absent), so an addition (blank old cell) or removal (blank new cell) is visible at a glance; column 4 is the signed byte delta. The column headers are the two image basenames. A leading ~ volume row appears when the volume name differs:

~  volume  "v1" -> "v2"
    old.img         new.img         SIZE    DETAIL
-------------------------------------------------------
+                   bin/newtool     +12345B exec
~   etc/init.d/rcS  etc/init.d/rcS  -4B     exec  (same size, content differs)
-   etc/old.conf                    -89B
~   init            init            -       rcS -> rc.local
~   lib/libfoo.so   lib/libfoo.so   0B      (same size, content differs)
=   var/run         var/run         -       f->l
-------------------------------------------------------
TOTAL  +1 added  -1 removed  ~4 changed  =1 type-changed
image: A=65536B  B=65536B
disk:  A=131072B  B=131072B
size:  +12345B  -93B  -> +12252B

The far-left symbol is + added (in new only), - removed (in old only), ~ changed (both sides), = type-changed (both sides); a blank path cell on either side means that image lacks the entry. The SIZE column is the signed byte delta (+N grew, -N shrank, 0B unchanged size); - means size does not apply (symlink / type-change / volume). The DETAIL column carries the executable bit (exec, or exec:true->false when it flips), dir for directories, symlink targets (-> target or old -> new), the type pair for a type-change (f->l), and the (same size, content differs) flag — regular files of equal size are compared byte-for-byte, so same-size content edits are surfaced explicitly. The trailing footer reports three independent size bases on separate lines so they are not conflated: image: is each image's full_size (the romfs superblock's declared volume size — header, node metadata, payload, 16-byte alignment); disk: is the on-disk file size, which may exceed full_size when the file was truncate-padded to a flash-erase-block boundary; size: is the signed byte delta of regular-file payload only (+XB appeared, -YB disappeared, -> the signed net). size: does NOT equal disk_b - disk_a, because image size also includes header, metadata, and alignment — which is why the three are split.

CLI reference

romfs <image> [--verify]                  print header info + per-entry space breakdown
romfs list   <image> [--sort S] [--all]   list the file tree (size-0 hidden without --all)
romfs unpack <image> [-o OUTDIR] [PATH]   unpack all, or one entry to stdout
romfs pack <srcdir> -o <image> [-n NAME]  build an image from a directory tree
romfs diff <a.img> <b.img> [--text]       compare two images file-by-file (exit 1 if different)

romfs -V prints the version; romfs -h shows help. list --sort orders by name (default), size (largest first), or none (on-disk order); --all also shows size-0 entries (directories and empty files). To inspect file contents, use unpack; the bare form and --verify only print image-level metadata.

Library API

from romfs import RomFSReader, RomFSWriter, EntryType, diff_images

# Read an image
with RomFSReader("rootfs.img") as r:
    print(r.volume_name, r.full_size)
    for path, node in r.walk():
        print(f"{node.type.name:9} {node.size:>8}  {path}")
    # File contents are sliced from the mmap on demand
    data = r.read(r.root.children[0])

    # Path-based access: find a node, read its payload, list a directory.
    node = r.find("etc/init.d/rcS")     # None if absent
    if node is not None:
        print(node.path, node.executable)
        print(r.read_path("etc/init.d/rcS"))
    print([n.name for n in r.listdir("etc")])

# Build an image from a directory tree
RomFSWriter.from_directory("./rootfs", volume_name="myvol").write("out.img")

# Compare two images: added / removed / changed / type-changed paths.
# Regular files of equal size are compared byte-for-byte, so same-size
# content edits are caught, not just size changes.
with RomFSReader("old.img") as a, RomFSReader("new.img") as b:
    for entry in diff_images(a, b):
        print(entry.status, entry.path)

Supported entry types

Type Read Write
Regular file yes yes
Directory yes yes
Symlink yes yes
Hardlink / device / socket / fifo yes no

[!NOTE] romfs stores only the executable bit (no full Unix mode), so the kernel assigns fixed default modes for the rest. The writer preserves the exec bit from the source file (set when any execute bit is on, matching genromfs); other permission bits are not preserved. Hardlinks and special files are parsed on read but not emitted on write.

Development

pip install -e ".[dev]"        # pytest, black, ruff, mypy, build, twine
pytest                          # runs against src/ directly, no install needed
black src tests main.py         # format
ruff check src tests            # lint
mypy src                        # type check
python -m build                 # build sdist + wheel into dist/

CI runs black + ruff + mypy + pytest across Python 3.10-3.13, plus a packaging job that builds the wheel and tests the installed package. Releases publish to PyPI automatically on a GitHub Release via Trusted Publishing (OIDC).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

romfs-0.0.6.tar.gz (33.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

romfs-0.0.6-py3-none-any.whl (28.3 kB view details)

Uploaded Python 3

File details

Details for the file romfs-0.0.6.tar.gz.

File metadata

  • Download URL: romfs-0.0.6.tar.gz
  • Upload date:
  • Size: 33.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for romfs-0.0.6.tar.gz
Algorithm Hash digest
SHA256 75f315ffc3e7ad951548d7e1842845f69fa08fc2107aafa3942442ee5e4eebcc
MD5 9efe87dd26a3b58cc61a8e775600e851
BLAKE2b-256 837ded4fcd46d1c11f57bd303d1a98c44180ed01eeb5d68b302ae4fec1062847

See more details on using hashes here.

File details

Details for the file romfs-0.0.6-py3-none-any.whl.

File metadata

  • Download URL: romfs-0.0.6-py3-none-any.whl
  • Upload date:
  • Size: 28.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for romfs-0.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 995ca5dd3c9566e73b98a117038ac1ddf425de66e130f2214e587c9d0c008e7c
MD5 d04468809de58b7798861a07a0b2cccd
BLAKE2b-256 a235d39f3cf90a577dd355322ab3c83bf05e14ce7e87a2e4362903a6b8e20b4b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page