Skip to content

Instantly share code, notes, and snippets.

@arpruss
Last active January 10, 2026 13:31
Show Gist options
  • Select an option

  • Save arpruss/d61c6830a15b7153fec3eff7a57b9737 to your computer and use it in GitHub Desktop.

Select an option

Save arpruss/d61c6830a15b7153fec3eff7a57b9737 to your computer and use it in GitHub Desktop.
lifutils.py
import sys
import struct
BLOCK_SIZE = 256
DIR_ENTRY_SIZE = 32
RESERVED_TRACKS = 1
CHUNKING = True
CHUNK_FILLER = b'\xFF\xFF' + (BLOCK_SIZE-2)*b'\x00'
if len(sys.argv) < 3:
print("python lifutils.py dir lifname.lif")
print("python lifutils.py rm lifname.lif FILE_TO_DELETE")
print("python lifutils.py get lifname.lif FILE_TO_GET [host_filename]")
print("python lifutils.py put lifname.lif [host_filename] FILE_TO_PUT FileType")
print("python lifutils.py pack lifname.lif")
sys.exit(1)
cmd = sys.argv[1]
def bytesToWord(b):
return b[0] << 8 | b[1]
def wordToBytes(w):
return bytes( ( w >> 8, w & 0xFF ) )
class DirEntry:
def __init__(self, *args):
if len(args) == 0:
self.name = ""
self.fileType = 1
self.startBlock = 0
self.blocks = 0
self.year = 0x90
self.month = 1
self.day = 1
self.hour = 1
self.minute = 1
self.second = 1
self.misc = bytes.fromhex("8001534f544f")
self.unchunkedFile = bytearray()
self.chunkedFile = bytearray()
else:
name,self.fileType,self.startBlock,self.blocks,self.year,self.month,self.day,self.hour,self.minute,self.second,self.misc = struct.unpack(">10sHII6B6s", args[0])
self.name = name.decode("ascii").strip()
self.chunkedFile = diskData[self.startBlock*BLOCK_SIZE:(self.startBlock+self.blocks)*BLOCK_SIZE]
self.unchunkedFile = unchunkFile(self.chunkedFile)
def __str__(self):
return "%s,%04X,%u,%u,%u,%02x/%02x/%02x %02x:%02x:%02x,%s" % (self.name,self.fileType,
self.startBlock,self.blocks,len(self.unchunkedFile),
self.year,self.month,self.day,self.hour,self.minute,self.second,
self.misc.hex())
def toBinary(self):
outName = (self.name + (10-len(self.name))*' ').encode("ascii")
return struct.pack(">10sHII6B6s", outName,self.fileType,self.startBlock,self.blocks,self.year,self.month,self.day,self.hour,self.minute,self.second,self.misc)
def chunkFile(unchunked):
if not CHUNKING:
return unchunked
chunked = bytearray()
pos = 0
while pos < len(unchunked):
if pos + BLOCK_SIZE - 2 <= len(unchunked):
size = BLOCK_SIZE - 2
else:
size = len(unchunked) - pos
chunked += wordToBytes(size)
chunked += unchunked[pos:pos+size]
pos += size
n = len(chunked) % BLOCK_SIZE
if n % BLOCK_SIZE != 0:
chunked += CHUNK_FILLER[:BLOCK_SIZE - n]
return chunked
def unchunkFile(chunked):
if not CHUNKING:
return chunked
unchunked = bytearray()
pos = 0
while pos < len(chunked):
size = bytesToWord(chunked[pos:pos+2])
unchunked += chunked[pos+2:pos+2+size]
if size < BLOCK_SIZE - 2:
break
pos += 2+size
return unchunked
rewrite = False
def fillFF(start,length):
for j in range(length):
diskData[start+j] = 0xFF
def delete(name):
for i in range(len(directory)):
if name == directory[i][1].name:
offset = DIR_ENTRY_SIZE * directory[i][0]
diskData[dirStart * BLOCK_SIZE + offset + 10] = 0
diskData[dirStart * BLOCK_SIZE + offset + 11] = 0
return True
return False
def get(inFile,outFile):
for i in range(len(directory)):
if inFile == directory[i][1].name:
with open(outFile, "wb") as outf:
outf.write(directory[i][1].unchunkedFile)
return True
return False
def pack():
filePos = dirStart + dirBlocks
dirPos = 0
newDirectory = bytearray()
for _,entry in directory:
diskData[filePos*BLOCK_SIZE:filePos*BLOCK_SIZE + len(entry.chunkedFile)] = entry.chunkedFile
entry.startBlock = filePos
newDirectory += entry.toBinary()
filePos += entry.blocks
fillFF( filePos * BLOCK_SIZE, (totalBlocks - filePos) * BLOCK_SIZE)
diskData[dirStart * BLOCK_SIZE : dirStart * BLOCK_SIZE + len(newDirectory)] = newDirectory
fillFF( dirStart * BLOCK_SIZE + len(newDirectory), dirEntries * DIR_ENTRY_SIZE - len(newDirectory))
def put(inFile, outFile, fileType):
with open(inFile, "rb") as inf:
data = chunkFile(inf.read())
blocksNeeded = (len(data) + BLOCK_SIZE - 1) // BLOCK_SIZE
data += (blocksNeeded * BLOCK_SIZE - len(data)) * b'\xFF'
for i in range(len(directory)):
entry = directory[i][1]
if entry.name == outFile and entry.blocks == blocksNeeded:
entry.fileType = fileType
diskData[dirStart * BLOCK_SIZE + i * 32 : dirStart * BLOCK_SIZE + i * DIR_ENTRY_SIZE + DIR_ENTRY_SIZE] = entry.toBinary()
diskData[entry.startBlock * BLOCK_SIZE : (entry.startBlock + blocksNeeded) * BLOCK_SIZE] = data
print("Replaced in place")
return True
if delete(outFile):
print("Deleted original")
readDir(False)
print("Packing")
pack()
readDir(False)
if len(directory) + 1 > dirEntries:
print("Out of space in directory")
return False
if lastBlock+blocksNeeded > totalBlocks:
print("Needed %d, have %d blocks" % (blocksNeeded,totalBlocks-lastBlock))
print("No space!")
return False
newEntry = DirEntry()
newEntry.name = outFile
newEntry.fileType = fileType
newEntry.blocks = blocksNeeded
newEntry.startBlock = lastBlock
diskData[lastBlock*BLOCK_SIZE:lastBlock*BLOCK_SIZE+len(data)] = data
diskData[dirStart*BLOCK_SIZE + len(directory) * DIR_ENTRY_SIZE : dirStart * BLOCK_SIZE + len(directory) * DIR_ENTRY_SIZE + DIR_ENTRY_SIZE] = newEntry.toBinary()
return True
def readDir(quiet=False):
global directory,lastBlock
directory = []
startBlock = 0
lastBlock = dirStart + dirBlocks
for i in range(dirEntries):
offset = dirStart * BLOCK_SIZE + i * DIR_ENTRY_SIZE
if diskData[offset] != 0xFF and struct.unpack(">H", diskData[offset+10:offset+12])[0] != 0:
directory.append((i,DirEntry(diskData[offset:offset+DIR_ENTRY_SIZE])))
if directory[-1][1].startBlock + directory[-1][1].blocks > lastBlock:
lastBlock = directory[-1][1].startBlock + directory[-1][1].blocks
if not quiet:
print(directory[-1][0],str(directory[-1][1]))
print("Last block",lastBlock)
with open(sys.argv[2],"rb") as inf:
diskData = bytearray(inf.read())
lifHeader, name, dirStart, lifId, dirBlocks, dirVersion, tracks, sides, blocksPerTrack = struct.unpack(">H6sIH2xIH2x3I", diskData[:36])
dirEntries = dirBlocks * BLOCK_SIZE // DIR_ENTRY_SIZE
if tracks == 0:
print("assuming default geometry")
tracks = 80
sides = 2
blocksPerTrack = 20
diskData[24:36] = struct.pack(">3I",tracks,sides,blocksPerTrack)
totalBlocks = (tracks-RESERVED_TRACKS) * sides * blocksPerTrack
if lifHeader != 0x8000:
print("Not a valid lif file")
sys.exit(2)
if lifId != 0x1000:
print("Invalid lif ID %04x" % lifId)
print("Volume:",name.decode())
print("Directory start: %u\nDirectory length: %u blocks\nDirectory version: %u" % (dirStart,dirBlocks,dirVersion))
print("Tracks: %u\nSides: %u\nBlocks per track: %u" % (tracks,sides,blocksPerTrack))
readDir()
if cmd == "rm":
for f in sys.argv[3:]:
if delete(f):
rewrite = True
else:
print("File %s not found" % sys.argv[3])
elif cmd == "put":
if len(sys.argv) >= 6:
inFile = sys.argv[3]
outFile = sys.argv[4]
fileType = int(sys.argv[5],16)
else:
inFile = sys.argv[3]
outFile = sys.argv[3]
fileType = int(sys.argv[4],16)
if put(inFile,outFile,fileType):
rewrite = True
else:
print("Error putting %s -> %s" % (inFile,outFile))
elif cmd == "get":
if len(sys.argv) >= 5:
inFile = sys.argv[3]
outFile = sys.argv[4]
else:
inFile = sys.argv[3]
outFile = sys.argv[3]
if not get(inFile,outFile):
print("Error getting %s -> %s" % (inFile,outFile))
elif cmd == "pack":
pack()
rewrite = True
if rewrite:
print("rewriting")
readDir()
with open(sys.argv[2],"wb") as outf:
outf.write(diskData)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment