Skip to content

Storage Format en

Won-Kyu Park edited this page Aug 4, 2026 · 3 revisions

Storage Format

What a .srcs file actually looks like on disk, and what happens byte-by-byte when you commit. This is the code-level follow-up to the "reverse delta" idea introduced in System Overview.

Header

A v2 file starts with a header line:

# SimpleRCS v2.0; hash_algo=sha256; encoding=utf-8;

v1 is the older, headerless format (see "How v1 differs" below).

What a block looks like

A block is a series of key @value@; lines, terminated by a blank line before the next block. The @ character gets escaped as @@ inside a value.

ver @1.2@;
date @2026-08-04T12:00:00@;
author @wkpark@;
log @fix typo@;
delta @0a1,1
> new line
@;
prev_hash @9f86d0...@;
hash @a3f5c2...@;

Field breakdown:

  • ver, date, author, log — always present
  • text — this block holds full text (a text HEAD)
  • delta — this block holds a delta (the instructions to go from the next newer version back to this one)
  • binary — used instead of text/delta when the content is binary. Format is <length>;base64,<encoded data>
  • prev_hash, hash — v2 only. Previous block's hash and this block's hash
  • signature — repeated once per GPG signature, if any are attached

What the hash actually covers

This part is a bit subtle. The hash is computed over the version's logical full text, not whatever is physically stored (see codec.calculate_block_hash). The payload is ver|date|author|log|<full text>|<previous hash>, concatenated in that order and hashed.

The reason: when a block that used to be HEAD gets converted into a delta on the next commit, its hash value must not change. The stored representation changed, but the actual content that version held didn't — so the hash has to stay the same, or the chain breaks.

What happens on disk during a commit

When commit() runs (inside simple_rcs.py's commit method):

  1. Re-encode the current HEAD as a delta block (_format_block with is_delta=True)
  2. Encode the new content as a full-text block (is_delta=False)
  3. Seek the stream back to where the old HEAD block started (stream.seek(head_info["start"]))
  4. Write "the re-encoded old HEAD" + "the new HEAD" in a single write() call
  5. truncate() to drop whatever tail is left over
self.stream.seek(self.head_info["start"])
self.stream.write(old_block_bytes + new_block_bytes)
self.stream.truncate()

The code comments explain why it's one combined write() — splitting it into two writes would leave a half-finished state (old HEAD overwritten, new HEAD missing) if the process died in between.

Net effect: every commit rewrites exactly one block — the last one — and everything before that stays right where it already was in the file. It looks like "keep appending to the end," but it's more precisely "replace the last block, then append a new one."

Snapshots

Passing commit(..., snapshot=True) changes step 3: instead of a delta, the old HEAD gets stored as full text. That breaks the delta chain at that point — reading that version means reading full text directly, no deltas to apply. If you know a section of history is going to get long and frequently read, taking a snapshot there buys back lookup performance for that stretch. It's opt-in per commit, not automatic.

Reading an old version

checkout(ver) starts at HEAD and walks backward one block at a time, applying each delta, until it hits the target version (_get_prev_block). If it hits a snapshot block along the way, it jumps straight to the full text there. It never loads the whole file into memory — it only reads the blocks it needs, in order.

How v1 differs (read-only support, historical reference)

v1 was an early format that was never actually shipped — no header, and delta blocks used the same text key as full-text blocks (no separate delta key). No hash chain, no GPG signatures. simple_rcs.py still has self._version < 2 branches for reading it. The v1-to-v2 migration tool still exists in repository history (added and reverted as commits 0c7783e/90f2706) but isn't in the current tree.

Text and binary delta formats

  • Text: an RCS-style diff -n script — human-readable add/delete instructions.
  • Binary: a BSDIFF40-compatible patch, base64/base85-encoded and embedded inside a text block. Since the patch format itself is standard, it also interoperates with the native bsdiff/bspatch tools if you have them.

What this means in practice

Because revisions aren't full snapshots, total file size ends up closer to "sum of delta sizes + one full text" rather than "revision count × page size." A one-line edit typically only adds a few hundred bytes. Wiki Backend Design (Korean only) has real measurements of what this means when you're storing .srcs content in a database.

Clone this wiki locally