"""The demo: delete a file, write a three-byte one, read back the deleted
file's plaintext through the filesystem's own API.

Run:  python3 demo.py

Writes `disk.img` and `disk-cost.img` in the current directory. Both are
re-formatted from scratch on every run, so the output is byte-identical
each time.
"""

import os
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from tinyfs import FileSystem

IMG = "disk.img"
COST_IMG = "disk-cost.img"

# Obviously-fake credentials. The lesson is block recycling; the string just
# has to be recognisable when it reappears somewhere it shouldn't.
SECRET = (b"ROOT PASSWORD: hunter2-correct-horse\n"
          b"SSN: 123-45-6789\n"
          b"CARD: 4111 1111 1111 1111 exp 09/29 cvv 402\n")
NOTES = b"ok\n"


def rule(title):
    print("\n" + title)
    print("-" * len(title))


def show(fs, label):
    entries = fs.ls()
    listing = "  ".join("%s size=%d blocks=%s" % e for e in entries) or "(empty)"
    free = sum(1 for b in fs.bitmap() if not b)
    print("  %-22s %s   [free blocks: %d]" % (label, listing, free))


def trace(fs):
    """The six steps. Returns the bytes `read()` hands back at the end."""
    show(fs, "1. empty disk")
    fs.write("secret.txt", SECRET)
    show(fs, "2. write secret.txt")
    before = fs.block_writes
    fs.rm("secret.txt")
    print("  %-22s rm cost: %d block writes" % ("3. rm secret.txt",
                                                fs.block_writes - before))
    show(fs, "   after rm")
    fs.write("notes.txt", NOTES)
    show(fs, "4. write notes.txt")
    print("  %-22s %r" % ("5. read notes.txt", fs.read("notes.txt")))
    fs.truncate("notes.txt", fs.block_size)
    show(fs, "6. truncate to 64")
    return fs.read("notes.txt")


def leaked(data):
    """Bytes past the 3 we wrote that match the deleted file, position for
    position."""
    return sum(1 for i in range(len(NOTES), len(data))
               if i < len(SECRET) and data[i] == SECRET[i])


print("secret.txt is %d bytes; notes.txt is %d bytes; block size is 64."
      % (len(SECRET), len(NOTES)))

rule("A. Default mount: no zeroing anywhere")
data = trace(FileSystem.format(IMG))
print("\n  read('notes.txt') now returns %d bytes:" % len(data))
print("    %r" % data)
tail = data[len(NOTES):]
print("  of which %d bytes are the deleted file's plaintext:" % leaked(data))
print("    %r" % tail)
print("  identical to SECRET[3:64]? %s" % (tail == SECRET[3:64]))

for letter, opt in (("B", "zero_on_free"), ("C", "zero_on_alloc")):
    rule("%s. Same trace, mounted with %s=True" % (letter, opt))
    data = trace(FileSystem.format(IMG, **{opt: True}))
    print("\n  read('notes.txt') returns %d bytes; %d of them are the deleted"
          " file." % (len(data), leaked(data)))
    print("  tail is %d zero bytes: %s"
          % (len(data) - len(NOTES),
             data[len(NOTES):] == b"\0" * (len(data) - len(NOTES))))

rule("D. What the two fixes cost, on a 4096-byte file")
print("  %-16s %14s %14s" % ("mount option", "write 4096B", "rm"))
for label, opts in (("(default)", {}),
                    ("zero_on_free", {"zero_on_free": True}),
                    ("zero_on_alloc", {"zero_on_alloc": True})):
    fs = FileSystem.format(COST_IMG, num_blocks=128, num_inodes=4,
                           direct=64, **opts)
    fs.write("big", b"A" * 4096)
    w = fs.block_writes
    fs.rm("big")
    print("  %-16s %11d bw %11d bw" % (label, w, fs.block_writes - w))

rule("E. Where it vanishes: the new file's length, same trace")
for n in (3, 62, 63, 64):
    fs = FileSystem.format(IMG)
    fs.write("secret.txt", SECRET)
    fs.rm("secret.txt")
    fs.write("notes.txt", b"x" * n)
    fs.truncate("notes.txt", 64)
    got = fs.read("notes.txt")
    stale = sum(1 for i in range(n, 64) if got[i] == SECRET[i])
    print("  new file %2d bytes -> %2d stale bytes exposed" % (n, stale))
