EROFS images written and read in pure OCaml
44%
OCaml 41%
C 10%
Shell 2%
C++ 2%
Roff <1%
Dune <1%

README.md

erofs #

EROFS images written and read in pure OCaml.

I wanted read-only root filesystems whose updates stay small: change one file, ship roughly that file. EROFS, the read-only filesystem in the Linux kernel, can do it if the image is laid out with care, and this library writes such images. Erofs.write gives you two strings. meta holds the superblock, the inodes and the directories, plus any small file, which sits right after its inode. data holds the 4 KiB pages of the remaining regular files. Glue them together with Erofs.device and the kernel mounts the result as one block device (mount -t erofs, no device= option needed).

Layout #

Each large file is stored chunk-based. Its inode is followed by an 8-byte index per page, and each index points into the data device, which the device table maps just past the metadata. Page addresses come from the names and the sizes in the tree. File contents never move anything, so if you rewrite some bytes of a file in place, only those data pages change and the metadata image is byte-for-byte the same.

Size changes are handled by ~previous. Give write the metadata image of the release you are replacing and it keeps each inode and each page where it was, as long as it still fits. When one file grows or shrinks, the new image differs from the old one in four places: that file's pages, its inode, the directory entry naming it, and block 0 (the superblock). How many other files the tree has makes no difference to the size of the update.

Small files skip the data device. A compact inode (struct erofs_inode_compact) takes 32 bytes, and the kernel will not read inline data that runs across a 4096-byte block boundary. An inode placed at the start of a block has 4096 - 32 = 4064 bytes after it, which is Erofs.inline_max. A file up to that size costs its own length in the metadata, where a chunk-based file would cost a full page.

Nothing else goes into the output: the same tree, timestamp and previous image always give the same bytes. Erofs.read parses these images and uncompressed images made by mkfs.erofs, and bounds-checks every offset it follows.

Installation #

Install with opam:

$ opam install erofs

If opam cannot find the package, it may not yet be released in the public opam-repository. Add the overlay repository, then install it:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install erofs

Usage #

let image =
  Erofs.write
    (Erofs.dir
       [ ("hello", Erofs.file "hello, world\n");
         ("bin", Erofs.dir [ ("sh", Erofs.symlink "/bin/busybox") ]) ])

let () =
  match Erofs.read (Erofs.device image) with
  | Ok _ -> ()
  | Error (`Msg e) -> failwith e

Testing against Linux #

test/linux_check.sh runs fsck.erofs on written images, then mounts them and walks the mount, checking each entry against the tree it was built from.

References #

  • Linux, fs/erofs/erofs_fs.h and Documentation/filesystems/erofs.rst.
  • erofs-utils, mkfs.erofs --chunksize --blobdev.