#
# xfs.py
#	Extract Filesystem
#
# Notes on filesystem structure:
# Every filesystem is just a directory; every directory is just its
#  own mini-filesystem.
# Both files and directories have a notion of free space which
#  they can allocate; "here" is what's currently allocated,
#  "fence" is the limit of what can be allocated.
# Thus, a file and directory (i.e., filesystem) have an allocation
#  of blocks at creation, and that's the limit of their growth.
# If you have a block of type "file", and "here" is 0, this is
#  a block within the file.  Walk backward until you find the block
#  with a non-zero "here", and that's the first block of this file.
# Free blocks under a dir or file have types FT_DIRFREE and FT_FILEFREE
#  respectively.
#
import sys, struct, os
import pdb

# Magic value to stamp on nodes
FSMAGIC = 0xFFEEEEDD

# Length in chars of filenames (max)
# As a counted string, it's really at most 31
NAMECHARS = 32

# Size of each filesystem block
BSIZE = 4096

# Initial part of BSIZE block, holding content
# (Metadata thus starts at DSIZE, for 96 bytes)
DSIZE = 4000

# Directories are always one block, thus this many filenames
#  at most (filename plus block #)
ENTSIZE = NAMECHARS + 4
NNAMES = DSIZE / ENTSIZE

# Storage size of a source (or shadow) screen
COLS = 80
ROWS = 25
SCR = ROWS*COLS

# Filesystem types
FT_DIR = 0
FT_FILE = 1
FT_DIRFREE = 2
FT_FILEFREE = 3

# Write lines from @s into @f
#
# @s points to 80x25 text; we conver to UNIX line
#  contentions here before writing.
def writelines(f, s):
    while s:
	l = s[:COLS]
	s = s[COLS:]

	# Trim trailing whitespace, but not indentation
	while l and (l[-1].isspace()):
	    l = l[:-1]

	f.write("%s\n" % (l,))

# A directory/filesystem block
class Dir(object):
    def __init__(self, bn, ents, here, fence):
	self.bn = bn
	self.ents = ents
	self.here = here
	self.fence = fence

# A file with content (body)
class File(object):
    def __init__(self, bn, body, here, fence):
	self.bn = bn
	self.body = body
	self.here = here
	self.fence = fence

# A ForthOS filesystem handler
class FS(object):
    def __init__(self, f, offset=0):
	self.f = f
	self.offset = offset

    # Decode FS entry at this position
    # Return a File or Dir
    def read(self, bn):
	bn -= self.offset
	self.f.seek(bn * BSIZE)
	buf = bytes(self.f.read(BSIZE))

	# Data
	body = buf[:DSIZE]

	# Metadata
	meta = buf[DSIZE:]
	magic,fstyp,fence,here = \
	    struct.unpack("<4L80x", meta)
	if magic != FSMAGIC:
	    raise Exception, "Failed filesystem magic check"

	# Directory
	if fstyp == FT_DIR:
	    ents = []
	    for x in xrange(0, NNAMES):
		idx = x*ENTSIZE
		buf2 = buf[idx:idx+ENTSIZE]
		nlen = ord(buf2[0])
		if not nlen:
		    # Empty slot
		    continue
		fn = str(buf2[1:nlen+1])
		bn2 = struct.unpack("<32xL", buf2)[0]
		ents.append( (fn, bn2) )
	    return Dir(bn, ents, fence, here)

	# File
	if fstyp == FT_FILE:
	    x = bn+1
	    bhere = here - self.offset
	    while x < bhere:
		self.f.seek(x * BSIZE)
		body2 = self.f.read(DSIZE)
		body += body2
		x += 1
		
	    # Pull in whole body of file
	    return File(bn, body, fence, here)

	# Deleted objects
	if fstyp in (FT_FILEFREE, FT_DIRFREE):
	    return None

	raise Exception, "Unknown filesystem element: %s" % (fstyp,)

    # Burst filesystem tree from this node downward
    def dump(self, bn, path):
	# Path to this object
	fpath = '/'.join(path)

	# See what it is
	pg = self.read(bn)
	if pg is None:
	    # Deleted file/dir
	    return

	if isinstance(pg, File):
	    # This @path leads to a file.  Assume it's
	    #  source, and split it to regular text
	    #  plus shadow source comments.
	    fn1 = fpath + '.txt'
	    f1 = open(fn1, "w")
	    fn2 = fpath + '.shd'
	    f2 = open(fn2, "w")
	    idx = 0
	    while idx < len(pg.body):
		src = pg.body[idx:idx+SCR]
		writelines(f1, src)
		shd = pg.body[idx+SCR:idx+SCR*2]
		writelines(f2, shd)
		idx += DSIZE
	    f1.close()
	    f2.close()
	    return

	# We're at a dir; create filesystem path to it
	assert isinstance(pg, Dir)
	depth = len(path)
	if depth:
	    os.system("mkdir -p %s" % (fpath,))
	for fn,bn2 in pg.ents:
	    sys.stdout.write("%s%s\n" % (" " * depth, fn))
	    self.dump(bn2, path + [fn])

if __name__ == "__main__":
    f = open(sys.argv[1], "r")
    fs = FS(f, 10000)
    fs.dump(10000, [])
