#!/usr/bin/python
import sys, os, struct, array, time
HEXFILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in xrange(256)])
def getbyte(d):
return struct.unpack("
def getword(d):
return struct.unpack("
def getdword(d):
return struct.unpack("
def dump(src, length=16):
N=0; result=''
while src:
s,src = src[:length],src[length:]
hexa = ' '.join(["%02X"%ord(x) for x in s])
s = s.translate(HEXFILTER)
result += "%04X: %-*s %s\n" % (N, length*3, hexa, s)
N+=length
return result
VERSION = "0.95"
class BlockDevice:
def __init__(self, file):
self.fd = open(file,"rb+")
self.written = {}
self.sectorsize = 512
def read(self, sector):
if sector in self.written:
return self.written[sector]
else:
self.fd.seek(sector*self.sectorsize)
data = self.fd.read(self.sectorsize)
return data
def mread(self, start, length):
self.fd.seek(start*self.sectorsize)
data = self.fd.read(self.sectorsize*length)
for i in xrange(length):
if (i+start) in self.written:
data = data[:i*self.sectorsize] + self.written[i+start] + data[i*self.sectorsize+self.sectorsize:]
return data
def write(self, sector, data):
if len(data) != self.sectorsize:
raise ValueError("Data size must equal sector size (%d, should be %d)"%(data,self.sectorsize))
old = self.read(sector)
if old == data:
return
self.written[sector] = data
def commit(self):
count = 0
for sector,data in self.written.items():
self.fd.seek(sector*self.sectorsize)
self.fd.write(data)
count += 1
self.written = {}
return count
def size(self):
self.fd.seek(0,2)
size = self.fd.tell()
if size%self.sectorsize != 0:
raise RuntimeError("Device size not divisible by sector size!")
return self.fd.tell()/self.sectorsize
def revert(self):
self.written = {}
def dumpchanges(self):
items = self.written.items()
items.sort(lambda x,y: cmp(x[0],y[0]))
for sector,data in items:
print "== SECTOR 0x%08X ==========================================================================="%sector
print dump(data)
class Partition:
def __init__(self, blockdevice, start, length, name, description=""):
self.blockdevice = blockdevice
self.start = start
self.length = length
self.name = name
self.description = description
def read(self, sector):
if sector >= self.length:
raise ValueError("Attempted to read data beyond partition end")
else:
return self.blockdevice.read(sector+self.start)
def mread(self, sector, length):
if (sector+length) > self.length:
raise ValueError("Attempted to read data beyond partition end")
else:
# d = ""
return self.blockdevice.mread(sector+self.start,length)
# for i in xrange(length):
# d += self.blockdevice.read(sector+self.start+i)
# return d
def write(self, sector, data):
if sector >= self.length:
raise ValueError("Attempted to write data beyond partition end")
else:
self.blockdevice.write(sector+self.start, data)
def mwrite(self, sector, data):
if len(data)%self.blockdevice.sectorsize != 0:
raise ValueError("Data size must be a multiple of the sector size")
else:
dsecs = len(data)/self.blockdevice.sectorsize
if (sector+dsecs) > self.length:
raise ValueError("Attempted to write data beyond partition end")
else:
for i in xrange(dsecs):
self.blockdevice.write(sector+i+self.start, data[i*self.blockdevice.sectorsize:(i+1)*self.blockdevice.sectorsize])
def __str__(self):
return "[%s] %d - %d (%d sectors, %.1fMB): %s"%(self.name,self.start,self.start+self.length-1,self.length,self.length/2048.0,self.description)
class PartitionCollection:
def __init__(self, blockdevice):
self.blockdevice = blockdevice
self.managers = []
self.partitions = []
def register(self, partitionmanager):
self.managers.append(partitionmanager)
self.partitions += partitionmanager.gettable()
def refresh(self):
self.partitions = []
for i in self.managers:
self.partitions += i.gettable()
def __getitem__(self, item):
return self.partitions.__getitem__(item)
def __len__(self):
return self.partitions.__len__()
def __str__(self):
s = "Partition Table:\n"
for i,part in enumerate(self.partitions):
s += " %d: %s\n"%(i+1,str(part))
return s
class MBRPartitionTable:
def __init__(self, device, prefix="MBR-", sector=0):
self.sector = sector
self.prefix = prefix
self.device = device
sector = self.device.read(self.sector)
sig = getword(sector[0x1FE:0x200])
if sig != 0xaa55:
raise RuntimeError("Invalid MBR Partition Table signature: 0x%04X"%sig)
def readentry(self, num):
if num >= 4:
raise ValueError("MBR Partition index out of bounds")
offset = num * 16 + 0x1be
entry = self.device.read(self.sector)[offset:offset+16]
boot,sh,scs1,scs2,ptype,eh,ecs1,ecs2,start,size = struct.unpack("
schs = (scs2 + ((scs1 & 0xc0) << 2),sh,scs1&0x3F)
echs = (ecs2 + ((ecs1 & 0xc0) << 2),eh,ecs1&0x3F)
return (boot&0x80==0x80),ptype,start,size,schs,echs
def writeentry(self, num, boot, ptype, start, size):
if num >= 4:
raise ValueError("MBR Partition index out of bounds")
offset = num * 16 + 0x1be
if boot:
bent = 0x80
else:
bent = 0x00
entry = struct.pack("
sector = self.device.read(self.sector)
sector = sector[:offset] + entry + sector[offset+16:]
self.device.write(self.sector,sector)
def hasentry(self, num):
if num >= 4:
raise ValueError("MBR Partition index out of bounds")
offset = num * 16 + 0x1be
entry = self.device.read(self.sector)[offset:offset+16]
boot,sh,scs1,scs2,ptype,eh,ecs1,ecs2,start,size = struct.unpack("
return ptype != 0x00
def gettable(self):
table = []
for i in xrange(4):
bootable,ptype,start,size,schs,echs = self.readentry(i)
if ptype != 0x00:
table.append(Partition(self.device,start,size,self.prefix+"%d"%(i+1),"MBR Partition of type 0x%02x"%ptype))
return table
class NonFatalFilesystemError(RuntimeError):
pass
class XBOXPartitionTable:
def __init__(self, device, useFG=False, prefix="XBOX-"):
self.prefix = prefix
self.device = device
self.useFG = useFG
self.devicesize = self.device.size()
if self.devicesize < 0x00EE8AB0:
raise RuntimeError("Device too small to be an XBOX Hard Disk: %d sectors"%self.devicesizee)
sector = self.device.read(3)
sig = sector[0:4]
if sig!="BRFR":
raise RuntimeError("Invalid XBOX Config Area signature: '%s'"%repr(sig))
def gettable(self):
table = []
table.append(Partition(self.device,0x00000000,0x00000003-0x00000000,self.prefix+"BOOT","XBOX Legacy Boot Area (MBR)"))
table.append(Partition(self.device,0x00000003,0x00000400-0x00000003,self.prefix+"CONFIG","XBOX Configuration Area"))
table.append(Partition(self.device,0x00000400,0x00177400-0x00000400,self.prefix+"X","XBOX Game Cache 1"))
table.append(Partition(self.device,0x00177400,0x002EE400-0x00177400,self.prefix+"Y","XBOX Game Cache 2"))
table.append(Partition(self.device,0x002EE400,0x00465400-0x002EE400,self.prefix+"Z","XBOX Game Cache 3"))
table.append(Partition(self.device,0x00465400,0x0055F400-0x00465400,self.prefix+"C","XBOX System"))
table.append(Partition(self.device,0x0055F400,0x00EE8AB0-0x0055F400,self.prefix+"E","XBOX Data"))
if self.devicesize > 0x00EE8AB0:
if self.devicesize <= 0x10000000:
table.append(Partition(self.device,0x00EE8AB0,self.devicesize-0x00EE8AB0,self.prefix+"F","XBOX Extended"))
else:
if self.useFG:
table.append(Partition(self.device,0x00EE8AB0,0x10000000-0x00EE8AB0,self.prefix+"F","XBOX Extended"))
table.append(Partition(self.device,0x10000000,self.devicesize-0x10000000,self.prefix+"G","XBOX LBA48 Extended"))
else:
table.append(Partition(self.device,0x00EE8AB0,self.devicesize-0x00EE8AB0,self.prefix+"F","XBOX Extended"))
return table
class Filesystem:
def __init__(self, partition):
self.partition = partition
class FATX(Filesystem):
def __init__(self, partition):
Filesystem.__init__(self, partition)
self.bootblock = partition.read(0)
self.justFormatted = False
if self.bootblock[0:4] != "FATX":
print "This FATX filesystem is not formatted."
yesno = raw_input("Do you want to format it? (Yes/No) (You need to type 'Yes'): ")
if yesno == "Yes":
print "Writing header..."
import random
self.bootblock = struct.pack("<4sIIHI","FATX",random.randint(0,2**32-1),32,1,0)
self.bootblock += "\xff"*0xFEE
self.partition.mwrite(0,self.bootblock)
print "Writing FAT..."
self.readfsinfo()
self.showinfo()
self.fat = array.array(self.arraycode,((self.dataclusters+1))*[self.fat_unused])
self.fat[0] = self.fat_eoc
self.fat[1] = self.fat_eoc_set # let's do things the MS way...
fatstring = self.fat.tostring()
fatstring += ((self.fatsize*512)-len(fatstring))*"\xff"
self.partition.mwrite(8,fatstring)
print "Writing Root Directory..."
self.putcluster(self.rootdir,"\xff"*self.clustersize*512)
print "Done!"
self.justFormatted = True
return
else:
raise NonFatalFilesystemError("Invalid FATX signature")
self.readfsinfo()
def readfsinfo(self):
self.volumeid, self.clustersize, self.fatcopies = struct.unpack("IIH",self.bootblock[4:14])
if self.fatcopies != 1:
raise NonFatalFilesystemError("FATX has %d FAT copies, only 1 supported"%self.fatcopies)
self.numclusters = self.partition.length / self.clustersize
if self.numclusters >= 0xfff4:
self.fatbits = 32
self.arraycode = "I"
self.fat_unused = 0x00000000L
self.fat_eoc = 0xfffffff8L
self.fat_eoc_set = 0xffffffffL
self.fat_reserved = 0xfffffff0L
self.fat_bad = 0xfffffff7L
else:
self.fatbits = 16
self.fat_unused = 0x0000
self.fat_eoc = 0xfff8
self.fat_eoc_set = 0xffff
self.fat_reserved = 0xfff0
self.fat_bad = 0xfff7
self.arraycode = "H"
self.fatsize = self.numclusters * self.fatbits/8
if self.fatsize % 4096 != 0:
self.fatsize = ((self.fatsize / 4096) + 1) * 4096
self.fatsize /= 512
self.dataoffset = self.fatsize + 8 - self.clustersize
self.rootdir = 1
self.dataclusters = (self.partition.length - self.fatsize -

/self.clustersize
#print dump(self.partition.mread(8,2))
def getcluster(self, num):
if num < 1:
raise ValueError("Invalid FAT cluster number %d"%(num))
return self.partition.mread(self.dataoffset+(num)*self.clustersize, self.clustersize)
def putcluster(self, num, data):
if num < 1:
raise ValueError("Invalid FAT cluster number %d"%(num))
if len(data) != 512*self.clustersize:
raise ValueError("Invalid FAT cluster size %d"%(num))
self.partition.mwrite(self.dataoffset+(num)*self.clustersize, data)
def showinfo(self):
print " FATX volume on %s: %.1fMB raw size"%(self.partition.name,self.partition.length/2048.0)
print " Volume ID: %08X"%self.volumeid
print " Cluster size: %d sectors (%dKB)"%(self.clustersize,self.clustersize/2)
print " FAT type: %d bits"%(self.fatbits)
print " FAT copies: %d"%(self.fatcopies)
print " FAT size: %d sectors (%dKB)"%(self.fatsize,self.fatsize/2)
print " Total clusters: %d (%d data clusters)"%(self.numclusters,self.dataclusters)
def readchain(self, cluster):
data = ""
while cluster < self.fat_eoc:
#print "Reading chain, cluster = %d"%cluster
d = self.getcluster(cluster)
data += d
#print "Cluster length: %d"%len(d)
cluster = self.fat[cluster]
#print "Chain length: %d"%len(data)
return data
def writechain(self, cluster, data):
while cluster < self.fat_eoc:
self.putcluster(cluster, data[:self.clustersize*512])
data = data[self.clustersize*512:]
cluster = self.fat[cluster]
if data != "":
raise RuntimeError("Chain length mismatch when writing chain!")
def extendchain(self, cluster):
#print "Extending cluster chain %d..."%cluster
if cluster == self.rootdir:
raise RuntimeError("Root directory is full and cannot be extended! (Or so I hear, it may very well be possible)")
while self.fat[cluster] < self.fat_eoc:
cluster = self.fat[cluster]
#print "- Start %d"%cluster
for newcluster in xrange(len(self.fat)):
if self.fat[newcluster] == self.fat_unused:
print "- New %d"%newcluster
self.fat[newcluster] = self.fat_eoc_set
self.fat[cluster] = newcluster
return
else:
raise RuntimeError("No free clusters available")
def finddir(self, path):
cd = self.rootdir
path = path.replace("\\","/")
for element in path.split("/"):
if element == "":
continue
dirdata = self.readchain(cd)
for i in xrange(len(dirdata)/64):
namesz,attr,name,cluster,size,mtime,ctime,atime = struct.unpack("
if namesz == 0xff or namesz == 0x00:
return RuntimeError("Path element %s not found"%element)
if namesz > 42:
continue
name = name[:namesz]
if name.lower() == element.lower():
cd = cluster
break
else:
raise RuntimeError("Path element %s not found"%element)
return cd
def addfile(self, dircluster, filename, start, length):
dirdata = self.readchain(dircluster)
#print "SDir: %d"%len(dirdata)
dirdataextension = 0
if len(filename) > 42:
raise RuntimeError("Filename length exceeds maximum of 42: %s (%d)"%(filename,len(filename)))
pfilename = filename+"\xFF"*(42-len(filename))
# first check for existing file
for i in xrange(len(dirdata)/64):
namesz,attr,name,cluster,size,mtime,ctime,atime = struct.unpack("
if namesz == 0xff or namesz == 0:
break
if namesz > 42:
continue
name = name[:namesz]
if name.lower() == filename.lower():
raise RuntimeError("File %s already exists!"%filename)
# Now store the file
for i in xrange(len(dirdata)/64):
namesz,attr,name,cluster,size,mtime,ctime,atime = struct.unpack("
if namesz == 0xff or namesz == 0x00:
dirdata = dirdata[:i*64] + struct.pack("
#print "A Dir: %d"%len(dirdata)
if i == ((len(dirdata)/64) - 1):
dirdata += "\xFF"*(self.clustersize * 512)
dirdataextension += 1
#print "End-extending dir: %d %d"%(len(dirdata),dirdataextension)
dirdata = dirdata[:i*64+64] + 64*"\xFF" + dirdata[i*64+128:]
#print "B Dir: %d"%len(dirdata)
break
if namesz > 42:
dirdata = dirdata[:i*64] + struct.pack("
break
else:
dirdataextension += 1
dirdata = dirdata + struct.pack("
"\xFF"*(self.clustersize * 512 - 64)
#print "Extending dir: %d %d"%(len(dirdata),dirdataextension)
for i in xrange(dirdataextension):
self.extendchain(dircluster)
#print "EDir: %d"%len(dirdata)
self.writechain(dircluster,dirdata)
def reserve_area(self):
print "Reading FAT...",
sys.stdout.flush()
start = 8
sleft = self.fatsize - 8
left = self.fatsize - 8
np = 0.1
fstr = ""
self.fat = array.array(self.arraycode)
while left > 0:
qty = min(left,1024)
s = self.partition.mread(start,qty)
self.fat.fromstring(s)
if self.fatsize > 20000 and np < (1-(float(left)/sleft)):
print ("%d%%"%int((np+0.001)*100)),
sys.stdout.flush()
np += 0.1
start += qty
left -= qty
print "Done."
print "Analyzing free space...",
sys.stdout.flush()
self.freegroups = []
self.totalfree = 0
lastfree = False
freestart = 0
np = 0.1
for cluster,value in enumerate(self.fat):
if cluster % 4096 == 0:
if np < cluster/float(self.numclusters):
print ("%d%%"%int((np+0.001)*100)),
sys.stdout.flush()
np += 0.1
if value == self.fat_unused:
self.totalfree += 1
if not lastfree:
freestart = cluster
lastfree = True
elif lastfree:
self.freegroups.append((freestart, cluster-freestart))
lastfree = False
if lastfree:
self.freegroups.append((freestart, cluster+1-freestart))
print "Done."
print "%d clusters free (%.1fMB)"%(self.totalfree, (self.totalfree*self.clustersize)/2048.0)
if self.totalfree == 0:
print "No space available!"
return False
self.freegroups.sort(lambda x,y: cmp(x[1],y[1]))
maxblock = self.freegroups[-1][1]
print "Largest contiguous block available: %d clusters (%.1fMB)"%(maxblock,maxblock*self.clustersize/2048.0)
havevalue = False
while True:
if not havevalue:
value = raw_input("Target partition size (#{K,M,G}): ")
havevalue = False
mult = 1
if not value:
continue
if value[-1] == "B":
value = value[:-1]
if not value:
print "Invalid size value."
continue
if value[-1] in "KMG":
if value[-1] == "K":
mult = 1024
elif value[-1] == "M":
mult = 1024*1024
elif value[-1] == "G":
mult = 1024*1024*1024
value=value[:-1]
if not value:
print "Invalid size value."
continue
try:
numbytes = int(value)
except ValueError:
print "Invalid size value."
continue
numbytes *= mult
if numbytes % 512 != 0:
numsectors = numbytes / 512 + 1
numbytes = numsectors * 512
else:
numsectors = numbytes / 512
if numsectors % self.clustersize != 0:
numclusters = numsectors / self.clustersize + 1
else:
numclusters = numsectors / self.clustersize
if numclusters > maxblock:
print "Size is too large"
continue
print "You requested %d sectors (%.1fMB)"%(numsectors, numsectors/2048.0)
nextlarger = len(self.freegroups)-1
nextsmaller = 0
if numbytes < self.freegroups[nextsmaller][1]*self.clustersize*512:
nextsmaller = None
else:
while numbytes >= self.freegroups[nextsmaller][1]*self.clustersize*512:
nextsmaller += 1
nextsmaller -= 1
while numbytes <= self.freegroups[nextlarger][1]*self.clustersize*512:
nextlarger -= 1
if nextlarger == -1:
break
nextlarger += 1
print "Closest contiguous block larger than the requested size: %d clusters (%.1fMB)"%\
(self.freegroups[nextlarger][1],self.freegroups[nextlarger][1]*self.clustersize/2048.0)
if nextsmaller is not None:
print "Closest contiguous block smaller than the requested size: %d clusters (%.1fMB)"%\
(self.freegroups[nextsmaller][1],self.freegroups[nextsmaller][1]*self.clustersize/2048.0)
print "- Enter L to use the size of the closest larger contiguous block"
if nextsmaller is not None:
print "- Enter S to use the size of the closest smaller contiguous block"
print "- Enter T to use the specified target size"
if numsectors % self.clustersize != 0:
print "- Enter R to use the specified target size, rounded to the next cluster (this is free)"
value = raw_input("- Or enter a new target size value (#{K,M,G}): ")
if value in "lL":
block = self.freegroups[nextlarger]
elif value in "sS":
block = self.freegroups[nextsmaller]
elif value in "tTrR":
block = self.freegroups[nextlarger][0],numclusters
if value in "rR":
numsectors = numclusters * self.clustersize
else:
havevalue = True
continue
if (self.totalfree-block[1])*self.clustersize < 2048:
print
print "You should probably leave some free space. Who knows what will happen otherwise."
print "Try again."
continue
kpercluster = self.clustersize / 2
allocbytes = block[1] * self.clustersize * 512
if numbytes >= 2**31-1:
print
print "The requested size will not fit into a standard file in FATX."
print "Please choose among the following options:"
print " A) Use multiple 1GB files. This is safe."
print " B) Use multiple 2GB-%dK files. This is safe, but not very neat."%kpercluster
print " C) Use multiple 2GB files. This will show up as -2GB using the MS kernel."
print " D) Use multiple 4GB-%dK files. This will show up as -%dK using the MS kernel."%(kpercluster,kpercluster)
print " E) Choose a new partition size."
choice = raw_input("Your choice: ")
while choice not in "ABCDEabcde":
print "Invalid choice %s"%choice
choice = raw_input("Your choice: ")
if choice in "eE":
continue
elif choice in "aA":
chunksize = 2048*1024/self.clustersize
elif choice in "bB":
chunksize = (4096*1024/self.clustersize)-1
elif choice in "cC":
chunksize = 4096*1024/self.clustersize
elif choice in "dD":
chunksize = (8096*1024/self.clustersize)-1
&nb