├── .gitignore ├── Pi-Resize.txt ├── add_loader.py ├── claim_frags.py ├── explore.py ├── fcform.py ├── get_loader.py ├── objects.py ├── put_loader.py ├── utils.py └── walk.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | Loader 3 | 4 | -------------------------------------------------------------------------------- /Pi-Resize.txt: -------------------------------------------------------------------------------- 1 | Some notes on expanding the size of the Rasperry Pi CD card. This is all 2 | done at your own risk! 3 | 4 | The card image available from RISC OS Open is around 2GB, but if your 5 | memory card is larger than that you won't be able to use all of it. 6 | Because there is no easy way to resize a filecore disc, the only way to 7 | do so is to reformat it. However the Pi need a DOS format partition to 8 | boot from, and reformatting will also destroy the partition table, and 9 | there is a risk of corruption of the DOS partition as RISC OS doesn't 10 | know it's there. 11 | 12 | To resize the RISC OS card uses some python scripts on linux, plus using 13 | RISC OS itself. The scripts will need to run as root (or with sudo). 14 | 15 | Firstly on linux - copy the RISC OS Pi image onto the SD card. 16 | 17 | Run get_loader.py to extract the 'Loader' (this is the 18 | DOS area used by the Pi to boot). is the device of your card - 19 | it's usually /dev/mmcblk0. is the filename to store the loader 20 | in. It can be whatever you want. 21 | 22 | Put the card into the the Pi and boot into RISC OS. 23 | 24 | Copy the contents to a USB stick - use something that will preseve the 25 | file types, so an archive or RISC OS format memory stick. You DON'T need 26 | to copy !Boot.Loader, although nothing bad will happen if you do, it's 27 | an extra 48M of data to copy for no reason). You will also need to put 28 | on the stick anything needed to extract the archive, or make it 29 | self-extracting. Rememeber that you will have no boot sequence running 30 | at restore time. 31 | 32 | Reformat the SD card using Utils.Caution.!HForm it's SDFS (M), say no to 33 | keeping the shape and accept what it then offers - you want to make it 34 | bootable, and enable long filenames. 35 | 36 | After formatting, put the card back into the linux machine. 37 | 38 | run put_loader.py to put the loader you saved earlier 39 | back onto the card. Exactly where it needs to go depends on the LFAU, 40 | which depends on the size of the card, the script will work it out for 41 | you. 42 | 43 | run claim_frags.py to put the loader into the Filecore map. 44 | This marks the area used by the loader, and then 45 | 46 | run add_loader.py to put the 'Loader' file into the root 47 | directory. 48 | 49 | put the card back in the Pi and RISC OS should boot to a command line, 50 | type *Desktop and press enter. You can just OK the message about !BOOT 51 | not being found. 52 | 53 | You can use the 'Free' window to verify RISC OS is now using all the card. 54 | 55 | You should then be able to copy or extract the files back onto the card. 56 | 57 | You can move the 'Loader' file from the root back inside !Boot. If you 58 | ended up copying the loader file earlier, you should make sure you 59 | delete the one you copied back, and keep the one that the scripts put on 60 | the card for you. 61 | 62 | If you need to get a working system before you restore your files, put 63 | the card in the Linux machine and mount the DOS partion in linux and 64 | copy the self-extracting hard disc image (from 65 | https://www.riscosopen.org/content/downloads/common) onto the DOS 66 | partition. 67 | 68 | Unmount it, and put the card back into the Pi. You'll need to issue a 69 | *Desktop, then openn the 'Loader' file and copy the self-extracting 70 | image to the root of the card, set it's type to 'Utility' and run it. 71 | 72 | When it's extracted, move everything from HardDisc4 to the root, and 73 | delete the now-empty directory. 74 | 75 | Reboot and you should get a working basic RISC OS system. If so you can 76 | delete the self-extracting file and move the Loader file into !Boot. 77 | 78 | That will get the standard RISC OS 5 disc structure, but not the full Pi 79 | image that was on the card to start with. 80 | 81 | -------------------------------------------------------------------------------- /add_loader.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | import sys 4 | from objects import BigDir 5 | from utils import get_map 6 | 7 | if len(sys.argv) != 2: 8 | print("Usage: add_loader ") 9 | exit(1) 10 | 11 | fd = open(sys.argv[1], "r+b") 12 | 13 | fs_map = get_map(fd) 14 | 15 | root_frag_id = fs_map.disc_record.root >> 8 16 | root_sec_offset = fs_map.disc_record.root & 0xff 17 | 18 | root_locations = fs_map.find_fragment(root_frag_id, fs_map.disc_record.root_size) 19 | 20 | fd.seek((root_locations[0])[0]) 21 | 22 | root = BigDir(fd.read(fs_map.disc_record.root_size)) 23 | root.delete('Loader') 24 | root.add('Loader', 0xffffc856, 0xeadfc18c, 50331648, 3, 0x300) 25 | root.sequence += 1 26 | root.show() 27 | root.data() 28 | 29 | fd.seek((root_locations[0])[0]) 30 | fd.write(root.data()) 31 | -------------------------------------------------------------------------------- /claim_frags.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | import sys 4 | from objects import Map 5 | from utils import find_map 6 | 7 | fd = open("Loader", "r") 8 | fd.seek(0,2) 9 | loader_len = fd.tell() 10 | fd.close() 11 | 12 | print "Loader is {0} bytes long.".format(loader_len) 13 | 14 | if len(sys.argv) != 2: 15 | print("Usage: claim_frags ") 16 | exit(1) 17 | 18 | fd = open(sys.argv[1], "r+b") 19 | map_address, map_length = find_map(fd) 20 | fd.seek(map_address) 21 | fs_map = Map(fd.read(map_length)) 22 | 23 | loader_start = (fs_map.disc_record.idlen+1) * fs_map.disc_record.bpmb 24 | 25 | bits_needed = loader_len / fs_map.disc_record.bpmb 26 | start_bit = loader_start / fs_map.disc_record.bpmb 27 | last_bit = start_bit + bits_needed 28 | 29 | while start_bit * fs_map.disc_record.bpmb < loader_start: 30 | start_bit += 1 31 | 32 | while bits_needed * fs_map.disc_record.bpmb < loader_len: 33 | bits_needed += 1 34 | 35 | print "{0} map bits required for loader, from bit {1} to {2}.".format(bits_needed,start_bit,last_bit) 36 | 37 | zone = 0 38 | 39 | while True: 40 | zone_start, zone_end = fs_map.zone_range(zone) 41 | 42 | first_in_zone = zone_start 43 | last_in_zone = zone_end 44 | 45 | if zone_start < start_bit: 46 | first_in_zone = start_bit 47 | 48 | if last_bit < last_in_zone: 49 | last_in_zone = last_bit 50 | 51 | #note = "" 52 | #if first_in_zone > zone_start: 53 | # note = " ** {0} bits not used at start of zone".format(first_in_zone-zone_start) 54 | 55 | #if last_in_zone < zone_end: 56 | # note = " ** {0} bits not used at end of zone".format(zone_end-last_in_zone) 57 | 58 | #print "Zone {0} - bits {1} to {2}{3}".format(zone,first_in_zone,last_in_zone,note) 59 | #print zone_start 60 | 61 | fs_map.allocate(zone, 3, first_in_zone-zone_start, last_in_zone-zone_start) 62 | 63 | if zone_end > last_bit: 64 | break 65 | 66 | zone += 1 67 | 68 | fd.seek(map_address) 69 | fd.write(fs_map.data.tostring()) 70 | 71 | -------------------------------------------------------------------------------- /explore.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | from datetime import datetime 4 | from utils import get_map 5 | from objects import BigDir, BootBlock 6 | import sys 7 | import cmd 8 | 9 | if len(sys.argv) != 2: 10 | print("Usage: explore ") 11 | exit(1) 12 | 13 | fd = open(sys.argv[1], "rb") 14 | fd.seek(0xc00) 15 | bb = BootBlock.from_buffer_copy(fd.read(0x200)) 16 | fd.seek(0) 17 | 18 | fs_map = get_map(fd) 19 | # fs_map.disc_record.show() 20 | 21 | root_fragment = fs_map.disc_record.root >> 8 22 | root_locations = fs_map.find_fragment(root_fragment, fs_map.disc_record.root_size) 23 | 24 | fd.seek((root_locations[0])[0]) 25 | 26 | root = BigDir(fd.read(fs_map.disc_record.root_size)) 27 | 28 | csd = root 29 | path = [root] 30 | 31 | csd.show() 32 | 33 | class Shell(cmd.Cmd): 34 | intro = '' 35 | prompt = '$> ' 36 | 37 | def do_dir(self, arg): 38 | 'Change into the specified directory.' 39 | global csd 40 | dirent = csd.find(arg.encode('latin-1')) 41 | if not dirent: 42 | print("Not found.") 43 | return 44 | if not dirent.is_directory(): 45 | print("Not a directory.") 46 | return 47 | 48 | location = fs_map.find_fragment(dirent.ind_disc_addr >> 8, dirent.length)[0] 49 | fd.seek(location[0]) 50 | 51 | csd = BigDir(fd.read(dirent.length)) 52 | path.append(csd) 53 | 54 | def do_up(self, arg): 55 | 'Change into the parent directory.' 56 | global csd 57 | global path 58 | if len(path) > 1: 59 | path = path[:-1] 60 | csd = path[-1] 61 | 62 | def do_cat(self, arg): 63 | 'List the contents of the current directory' 64 | csd.show() 65 | 66 | def do_zone(self, arg): 67 | 'Show info about a zone' 68 | zone = int(arg) 69 | print("Zone: {} - {} to {}".format(zone, *fs_map.zone_range(zone))) 70 | print("Header: {:02x} {:02x} {:02x} {:02x}".format(*fs_map.zone_header(zone))) 71 | fs_map.show_zone(zone, True) 72 | print("Zone check (calclated): {:02x}".format(fs_map.calc_zone_check(zone))) 73 | 74 | def do_quit(self, arg): 75 | 'Exits the exploerer.' 76 | return True 77 | 78 | def do_EOF(self, arg): 79 | return True 80 | 81 | def postcmd(self, stop, line): 82 | path_str = '' 83 | for item in path: 84 | if path_str != '': 85 | path_str += '.' 86 | path_str += item.name.decode('latin-1') 87 | 88 | self.prompt = path_str + "> " 89 | return cmd.Cmd.postcmd(self, stop, line) 90 | 91 | 92 | Shell().cmdloop() 93 | 94 | -------------------------------------------------------------------------------- /fcform.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | import sys 4 | import struct 5 | import ctypes 6 | import argparse 7 | 8 | from array import array 9 | from objects import DiscRecord, Map, BigDir, BootBlock 10 | from itertools import product 11 | 12 | 13 | ZONE0BITS = 60*8 # Bits used in Zone 0 14 | IDLEN_MAX = 21 15 | MAPSIZE_MAX = 128*1024*1024 16 | 17 | #### 18 | #disc_sectors = 234441648 19 | #log2_secsize = 9 20 | #### 21 | #disc_sectors = 1953525168>>3 # Convert to 4K 22 | #log2_secsize = 12 23 | #### 24 | 25 | def make_shape(sectors): 26 | """Generate a CHS shape for a given number of sectors.""" 27 | best_wasted = 0xffffffff 28 | best = None 29 | for secs,heads in product(range(63,15,-1), range(255,15,-1)): 30 | cyls = sectors / (secs*heads) 31 | if cyls > 65535: 32 | continue 33 | wasted = sectors % (secs*heads) 34 | if (wasted < best_wasted): 35 | best_wasted = wasted 36 | best = (secs,heads,sectors//(secs*heads)) 37 | return best 38 | 39 | def check_alloc(sectors, log2ss, zones, zonespare, log2bpmb, idlen): 40 | """Checks if the given allocation is valid.""" 41 | disc_allocs = (sectors << log2ss) // (1 << log2bpmb) 42 | bpzm = 8 * (1 << log2ss) - zonespare # Bits per zone map 43 | map_allocs = (bpzm * zones) - ZONE0BITS 44 | idpz = bpzm / (idlen+1) # IDs per Zone 45 | 46 | if map_allocs < disc_allocs: 47 | return False # Map doens't cover disc 48 | 49 | if (1 << idlen) < idpz * zones: 50 | return False # Not enough IDs 51 | 52 | excess_allocs = map_allocs - disc_allocs 53 | if 0 < excess_allocs <= idlen: 54 | return False # Can't make excess into a fragment 55 | 56 | lz_allocs = disc_allocs - (map_allocs - bpzm) 57 | if lz_allocs <= idlen: 58 | return False # Can't make last bit of last zone into a fragment 59 | 60 | return True 61 | 62 | def find_alloc(sectors, log2ss, zones, log2bpmb): 63 | """Find an allocation for the given zones/log2bpmb, or 64 | None if one cannot be found.""" 65 | for zonespare in range(32,64): # 32 to ZoneBits%-Zone0Bits%-8*8 66 | for idlen in range(log2_secsize, IDLEN_MAX+1): 67 | if check_alloc(sectors, log2ss, zones, zonespare, log2bpmb, idlen): 68 | return (zones, zonespare, log2bpmb, idlen) 69 | 70 | 71 | 72 | ## Start 73 | parser = argparse.ArgumentParser() 74 | parser.add_argument("device") 75 | parser.add_argument("sectors", type=int) 76 | parser.add_argument("--4k", dest="bigsecs", action="store_true") 77 | parser.add_argument("--lfau", type=int) 78 | 79 | args = parser.parse_args(sys.argv[1:]) 80 | disc_sectors = args.sectors 81 | log2_secsize = 12 if args.bigsecs else 9 82 | 83 | print("Disc has {} sectors of {} bytes - Capacity {:.1f} GB".format( 84 | disc_sectors, 1< Shape 96 | 97 | # Find what log2 bpmb / zones combinations we can use 98 | for log2bpmb in range(7,26): 99 | # Roughly how many zones we need 100 | map_bits_needed = (disc_sectors << log2_secsize) // (1 << log2bpmb) 101 | zones_guess = map_bits_needed // (1 << log2_secsize) // 8 102 | if zones_guess << log2_secsize > MAPSIZE_MAX: 103 | continue 104 | 105 | for zones in range(zones_guess, zones_guess + 10 + zones_guess // 20): 106 | alloc = find_alloc(disc_sectors, log2_secsize, zones, log2bpmb) 107 | if alloc: 108 | allocs[1< ") 10 | exit(1) 11 | 12 | fd = open(sys.argv[1], "rb") 13 | 14 | fd.seek(0, 0) 15 | 16 | part_table = fd.read(512) 17 | 18 | p1 = part_table[0x1be:0x1be+16] 19 | p2 = part_table[0x1ce:0x1ce+16] 20 | p3 = part_table[0x1de:0x1de+16] 21 | p4 = part_table[0x1ee:0x1ee+16] 22 | 23 | p1_data = struct.unpack("BBBBBBBBII",p1) 24 | 25 | start_sec = p1_data[8] 26 | length_sec = p1_data[9] 27 | 28 | start = start_sec * SECSIZE 29 | length = length_sec * SECSIZE 30 | 31 | print "Loader starts at sector {0} and is {1} sector ({2} bytes) long.".format(start_sec,length_sec,length) 32 | 33 | fd.seek(start) 34 | data = fd.read(length) 35 | 36 | fd.close() 37 | 38 | print "Saving 'Loader' file to",sys.argv[2] 39 | fd = open(sys.argv[2],"wb") 40 | fd.write(data) 41 | fd.close() 42 | 43 | -------------------------------------------------------------------------------- /objects.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import array 3 | import ctypes 4 | import random 5 | 6 | from functools import reduce 7 | 8 | def dir_check_words(data, count, dircheck): 9 | for word in struct.unpack("{0}I".format(count), data): 10 | dircheck = word ^ ( ((dircheck >> 13) & 0xffffffff) | ((dircheck << 19) & 0xffffffff) ) 11 | return dircheck 12 | 13 | def dir_check_bytes(data, count, dircheck): 14 | for byte in struct.unpack("{0}B".format(count), data): 15 | dircheck = byte ^ ( ((dircheck >> 13) & 0xffffffff) | ((dircheck << 19) & 0xffffffff) ) 16 | return dircheck 17 | 18 | class DiscRecord(ctypes.Structure): 19 | _fields_ = [ 20 | ('log2secsize', ctypes.c_uint8), 21 | ('secspertrack', ctypes.c_uint8), 22 | ('heads', ctypes.c_uint8), 23 | ('density', ctypes.c_uint8), 24 | ('idlen', ctypes.c_uint8), 25 | ('log2bpmb', ctypes.c_uint8), 26 | ('skew', ctypes.c_uint8), 27 | ('bootoption', ctypes.c_uint8), 28 | ('lowsector', ctypes.c_uint8), 29 | ('nzones_1', ctypes.c_uint8), 30 | ('zone_spare', ctypes.c_uint16), 31 | ('root', ctypes.c_uint32), 32 | ('disc_size_1', ctypes.c_uint32), 33 | ('disc_id', ctypes.c_uint16), 34 | ('disc_name', ctypes.c_char*10), 35 | ('disctype', ctypes.c_uint32), 36 | ('disc_size_2', ctypes.c_uint32), 37 | ('log2share', ctypes.c_uint8), 38 | ('big_flag', ctypes.c_uint8), 39 | ('nzones_2', ctypes.c_uint8), 40 | ('reserved_1', ctypes.c_byte), 41 | ('format_version', ctypes.c_uint32), 42 | ('root_size', ctypes.c_uint32), 43 | ('reserved_2', ctypes.c_byte*8), 44 | ] 45 | 46 | def __init__(self): 47 | self.density = 0 48 | self.skew = 0 49 | self.lowsector = 1 50 | self.big_flag = 1 51 | self.format_version = 1 52 | 53 | @staticmethod 54 | def from_bytes(data): 55 | return DiscRecord.from_buffer_copy(data) 56 | 57 | @property 58 | def bpmb(self): 59 | return 1 << self.log2bpmb 60 | 61 | @property 62 | def secsize(self): 63 | return 1 << self.log2secsize 64 | 65 | @property 66 | def share_size(self): 67 | return 1 << self.log2share; 68 | 69 | @property 70 | def nzones(self): 71 | return (self.nzones_2 << 8) + self.nzones_1 72 | 73 | @nzones.setter 74 | def nzones(self, new_zones): 75 | self.nzones_2 = new_zones >> 8 76 | self.nzones_1 = new_zones 77 | 78 | @property 79 | def disc_size(self): 80 | return (self.disc_size_2 << 32) + self.disc_size_1 81 | 82 | @disc_size.setter 83 | def disc_size(self, new_size): 84 | self.disc_size_2 = new_size >> 32 85 | self.disc_size_1 = new_size 86 | 87 | def show(self): 88 | print("Disc Record:") 89 | print(" Sector size: %d" % self.secsize) 90 | print(" Bytes per map bit: %d" % self.bpmb) 91 | print(" ID Length: %d" % self.idlen) 92 | print(" Skew: %d" % self.skew) 93 | print(" Low Sector: %d" % self.lowsector) 94 | print(" Number of zones: %d" % self.nzones) 95 | print(" Zone spare: %d" % self.zone_spare) 96 | print(" Share size: {}".format(self.share_size)) 97 | print(" Disc Size: %d (MB)" % (self.disc_size // 1024 // 1024)) 98 | print(" Root: {:x} ({} bytes)".format(self.root, self.root_size)) 99 | 100 | def map_info(self): 101 | return ( 102 | self.nzones // 2, 103 | ((self.nzones // 2)*(8*self.secsize-self.zone_spare)-480)*self.bpmb, 104 | self.secsize * self.nzones 105 | ) 106 | 107 | class Map(object): 108 | class Zone(object): 109 | def __init__(self, disc_record): 110 | self.disc_record = disc_record 111 | self.data = array.array('B', [0]*disc_record.secsize) 112 | 113 | 114 | def __init__(self, data): 115 | self.data = array.array('B') 116 | self.data.frombytes(data) 117 | self.disc_record = DiscRecord.from_bytes(data[4:64]) 118 | 119 | @property 120 | def disc_record(self): 121 | return DiscRecord.from_bytes(self.data[4:64]) 122 | 123 | @disc_record.setter 124 | def disc_record(self, disc_record): 125 | dr = bytearray(disc_record) 126 | for i in range(0,60): 127 | self.data[i+4] = dr[i] 128 | 129 | @property 130 | def nzones(self): 131 | return self.disc_record.nzones 132 | 133 | @property 134 | def id_per_zone(self): 135 | return ((self.disc_record.secsize*8) - self.disc_record.zone_spare) // (self.disc_record.idlen+1) 136 | 137 | def clear(self): 138 | cross_checks = random.randbytes(self.disc_record.nzones-1) 139 | cross_checks = cross_checks + int.to_bytes(0xff ^ reduce(lambda a,b:a^b, cross_checks), 1, "little") 140 | 141 | for zone in range(self.nzones): 142 | zone_start = zone * self.disc_record.secsize 143 | for n in range(64 if zone == 0 else 4, self.disc_record.secsize): 144 | self.data[zone_start+n] = 0x00 145 | 146 | last_bit = self.disc_record.secsize*8-self.disc_record.zone_spare-1 147 | self.set_bit(zone, last_bit, 1) 148 | 149 | start_bit = 0x18 + (480 if zone == 0 else 0) 150 | self.data[zone_start+1] = start_bit & 0xff 151 | self.data[zone_start+2] = (start_bit | 0x8000) >> 8 152 | 153 | self.data[zone_start+3] = cross_checks[zone] 154 | self.data[zone_start+0] = self.calc_zone_check(zone) 155 | 156 | def cross_check(self): 157 | cross_check = 0 158 | for zone in range(self.nzones): 159 | zone_start = zone * self.disc_record.secsize 160 | cross_check = cross_check ^ self.data[zone_start+3] 161 | 162 | return cross_check 163 | 164 | def show(self, unused=True): 165 | for zone in range(self.nzones): 166 | self.show_zone(zone,unused) 167 | 168 | def zone_range(self, zone): 169 | return (\ 170 | ((self.disc_record.secsize*8-self.disc_record.zone_spare)*zone) - (480 if zone > 0 else 0), 171 | ((self.disc_record.secsize*8-self.disc_record.zone_spare)*(zone+1)) - 480 - 1) 172 | 173 | def zone_header(self, zone): 174 | zone_start = zone * self.disc_record.secsize 175 | return self.data[zone_start:zone_start+4] 176 | 177 | def get_bit(self, zone, bit): 178 | byte = (bit // 8) + (64 if zone == 0 else 4) # Zone 0 has the disc record 179 | shift = bit % 8 180 | data = self.data[byte+zone*self.disc_record.secsize] 181 | val = data & 1 << shift 182 | return 1 if val > 0 else 0 183 | 184 | def set_bit(self, zone, bit, val): 185 | byte = (bit // 8) + (64 if zone == 0 else 4) # Zone 0 has the disc record 186 | shift = bit % 8 187 | old_data = self.data[byte+zone*self.disc_record.secsize] 188 | new_data = old_data & ( 0xff ^ 1<> self.disc_record.log2bpmb 194 | 195 | # which zone is it on? 196 | for zone in range(0, self.nzones): 197 | start, end = self.zone_range(zone) 198 | if start <= bit_in_map <= end: 199 | return (zone, bit_in_map-start) 200 | 201 | def calc_zone_check(self, zone): 202 | sum_vector0 = 0 203 | sum_vector1 = 0 204 | sum_vector2 = 0 205 | sum_vector3 = 0 206 | zone_start = zone * self.disc_record.secsize 207 | rover = ((zone+1)*self.disc_record.secsize)-4 208 | while rover > zone_start: 209 | sum_vector0 += self.data[rover+0] + (sum_vector3>>8) 210 | sum_vector3 &= 0xff 211 | sum_vector1 += self.data[rover+1] + (sum_vector0>>8) 212 | sum_vector0 &= 0xff 213 | sum_vector2 += self.data[rover+2] + (sum_vector1>>8) 214 | sum_vector1 &= 0xff 215 | sum_vector3 += self.data[rover+3] + (sum_vector2>>8) 216 | sum_vector2 &= 0xff 217 | rover -= 4 218 | 219 | # Don't add the check byte when calculating its value 220 | sum_vector0 += (sum_vector3>>8) 221 | sum_vector1 += self.data[rover+1] + (sum_vector0>>8) 222 | sum_vector2 += self.data[rover+2] + (sum_vector1>>8) 223 | sum_vector3 += self.data[rover+3] + (sum_vector2>>8) 224 | 225 | return (sum_vector0^sum_vector1^sum_vector2^sum_vector3) & 0xff 226 | 227 | def allocate(self, zone, frag_id, from_bit, to_bit): 228 | if to_bit - from_bit < self.disc_record.idlen: 229 | raise RuntimeError("allocation too small") 230 | 231 | free_link = ( (self.data[zone*self.disc_record.secsize+1] << 0) + 232 | (self.data[zone*self.disc_record.secsize+2] << 8) ) & 0x7fff 233 | 234 | #print("allocate zone {} {} to {} - FreeLink {:x}".format(zone, from_bit, to_bit, free_link)) 235 | 236 | free_offset = free_link - 0x18 - (480 if zone == 0 else 0) 237 | 238 | bit = from_bit 239 | for i in range(self.disc_record.idlen): 240 | self.set_bit(zone, bit, frag_id & (1< 0 else 0) 250 | #last_bit = ((self.disc_record.secsize*8-self.disc_record.zone_spare)*(zone+1)) -480 251 | 252 | last_bit = (self.disc_record.secsize*8-self.disc_record.zone_spare) - (481 if zone == 0 else 1) 253 | 254 | #print("from bit {} free_offset {} zone {}".format(from_bit, free_offset, zone)) 255 | if free_offset == from_bit: 256 | new_free_link = 0x8000 if bit == last_bit else 0x8001 + bit + (63*8 if zone == 0 else 3*8) 257 | self.data[zone * self.disc_record.secsize+1] = (new_free_link >> 0) & 0xff 258 | self.data[zone * self.disc_record.secsize+2] = (new_free_link >> 8) & 0xff 259 | else: 260 | self.set_bit(zone, from_bit-1, 1) 261 | print("Zone {}: from bit ({}) isn't start of free space in zone ({})".format(zone, from_bit, free_offset)) 262 | 263 | self.data[zone*self.disc_record.secsize+0] = self.calc_zone_check(zone) 264 | 265 | def show_zone(self, zone, show_unused): 266 | zone_start = zone * self.disc_record.secsize 267 | bits_before = ((self.disc_record.secsize*8-self.disc_record.zone_spare)*zone) - (480 if zone > 0 else 0) 268 | last_bit = ((self.disc_record.secsize*8-self.disc_record.zone_spare)*(zone+1)) -480 269 | free_link = (self.data[zone_start+2])*256+(self.data[zone_start+1]) 270 | 271 | if free_link == 0x8000: 272 | free_offset = None 273 | else: 274 | free_offset = (free_link & 0x7fff) - (63*8 if zone == 0 else 3*8) 275 | 276 | if free_offset is not None: 277 | print(("Zone %d (FreeLink = %x - %d bits)" % (zone, free_link, free_offset))) 278 | else: 279 | print(("Zone %d (FreeLink = %x - No free space)" % (zone, free_link))) 280 | 281 | bit = 0 282 | while True: 283 | frag_id = 0 284 | start = bit 285 | 286 | for i in range(self.disc_record.idlen): 287 | frag_id |= self.get_bit(zone, bit) << i 288 | bit += 1 289 | 290 | while (self.get_bit(zone, bit) == 0): 291 | if bit > last_bit: 292 | print("** Stop bit not found before end of zone.") 293 | break 294 | bit += 1 295 | 296 | disc_start = (start+bits_before)*self.disc_record.bpmb 297 | disc_end = (bit +bits_before)*self.disc_record.bpmb 298 | 299 | print((" Fragment ID: %x map bits %d to %d, disc address %d to %d (%d)" % 300 | (frag_id, start, bit, disc_start, disc_end-1, disc_end-disc_start))) 301 | 302 | bit += 1 303 | if bit+bits_before >= last_bit: 304 | break 305 | 306 | def find_in_zone(self, search_id, zone): 307 | zone_start = zone * self.disc_record.secsize 308 | bits_before = ((self.disc_record.secsize*8-self.disc_record.zone_spare)*zone) - (480 if zone > 0 else 0) 309 | free_link = (self.data[zone_start+2])*256+(self.data[zone_start+1]) 310 | 311 | rv = [] 312 | 313 | bit = 0 314 | while True: 315 | frag_id = 0 316 | start = bit 317 | 318 | for i in range(self.disc_record.idlen): 319 | frag_id |= self.get_bit(zone, bit) << i 320 | bit += 1 321 | 322 | while (self.get_bit(zone, bit) == 0): 323 | bit += 1 324 | 325 | bit += 1 326 | disc_start = (start+bits_before)*self.disc_record.bpmb 327 | disc_end = (bit +bits_before)*self.disc_record.bpmb 328 | 329 | if frag_id == search_id: 330 | rv.append((disc_start, disc_end)) 331 | 332 | if bit+bits_before >= ((self.disc_record.secsize*8-self.disc_record.zone_spare)*(zone+1)) -480: 333 | break 334 | 335 | return rv 336 | 337 | def find_fragment(self, fragment_id, length = None): 338 | addresses = [] # List of (start,end) disc addresses 339 | 340 | start_zone = fragment_id // self.id_per_zone 341 | zone = start_zone 342 | 343 | while True: 344 | in_this = self.find_in_zone(fragment_id, zone) 345 | if len(in_this) > 0: 346 | addresses += in_this 347 | 348 | if length: 349 | count = 0 350 | for (start,end) in addresses: 351 | count += end-start 352 | 353 | if count >= length: 354 | break 355 | 356 | zone += 1 357 | 358 | if zone >= self.disc_record.nzones: 359 | zone = 0 360 | 361 | if zone == start_zone: 362 | break 363 | 364 | return addresses 365 | 366 | class BigDir(object): 367 | class Entry(object): 368 | def __init__(self, name, loadaddr, execaddr, length, attribs, ind_disc_addr): 369 | self.name = name 370 | self.loadaddr = loadaddr 371 | self.execaddr = execaddr 372 | self.length = length 373 | self.attribs = attribs 374 | self.ind_disc_addr = ind_disc_addr 375 | 376 | @staticmethod 377 | def from_dir(data, name_heap): 378 | loadaddr, execaddr, length, ind_disc_addr, attribs,\ 379 | name_len, name_ptr = struct.unpack("IIIIIII",data) 380 | name = name_heap[name_ptr:name_ptr+name_len] 381 | return BigDir.Entry(name, loadaddr, execaddr, length, attribs, ind_disc_addr) 382 | 383 | def is_directory(self): 384 | return self.attribs & 1<<3 385 | 386 | def attr_str(self): 387 | s = '' 388 | s += "R" if self.attribs & 1<<0 else "-" 389 | s += "W" if self.attribs & 1<<1 else "-" 390 | s += "L" if self.attribs & 1<<2 else "-" 391 | s += "D" if self.attribs & 1<<3 else "-" 392 | s += "r" if self.attribs & 1<<4 else "-" 393 | s += "w" if self.attribs & 1<<5 else "-" 394 | return s 395 | 396 | def show(self): 397 | print('{0:<15} {1:08x} {2:08x} {3:12} {4} {5:x}'.format(\ 398 | self.name.decode('latin-1'), self.loadaddr, self.execaddr, 399 | self.length, self.attr_str(), self.ind_disc_addr)) 400 | 401 | def __init__(self, data=None): 402 | if data is None: 403 | self.sequence = 1 404 | self.size = 2048 405 | self.name = b'$' 406 | self.entries = [] 407 | return 408 | 409 | self.sequence, sbpr, name_len, self.size, \ 410 | entries, names_size, self.parent_id = struct.unpack("Bxxx4sIIIII",data[0:0x1c]) 411 | 412 | if sbpr != b'SBPr': 413 | raise RuntimeError("Invalid directory start marker ({0})".format(sbpr)) 414 | 415 | self.name = data[0x1c:0x1c+name_len] 416 | 417 | heap_start = (entries*0x1c) + ((0x1c+name_len+4)//4)*4 418 | heap_end = heap_start+names_size 419 | heap_data = data[heap_start:heap_end] 420 | 421 | self.entries = [] 422 | data_start = ((0x1C+name_len+4)//4)*4 423 | 424 | for entry in range(0,entries): 425 | start = data_start + (entry*0x1C) 426 | self.entries.append(\ 427 | BigDir.Entry.from_dir(data[start:start+0x1c], heap_data)) 428 | 429 | oven, end_seq, check = struct.unpack("4sBxxB",data[-8:]) 430 | 431 | if oven != b'oven': 432 | raise RuntimeError("Invalid directory end marker ({0})".format(oven)) 433 | 434 | tail = data[-8:] 435 | calc = dir_check_words(data[0:heap_end], heap_end//4, 0) 436 | calc = dir_check_words(tail[0:4], 1, calc) 437 | calc = dir_check_bytes(tail[4:7], 3, calc) 438 | calc = (calc << 0 & 0xff) ^ (calc >> 8 & 0xff) ^ (calc >> 16 & 0xff) ^ (calc >> 24 & 0xff) 439 | 440 | if calc != check: 441 | raise RuntimeError("Directory check-byte failed.") 442 | 443 | def data(self): 444 | # TODO: Currently only handles 2048 byte directories. 445 | data = b'' 446 | 447 | name_heap = b'' 448 | heap_lookup = {} 449 | for entry in self.entries: 450 | heap_lookup[self.entries.index(entry)] = len(name_heap) 451 | name_heap += entry.name+b'\x0d' 452 | 453 | # Word-justify it 454 | while len(name_heap) % 4 != 0: 455 | name_heap += b'\x00' 456 | 457 | seq = self.sequence 458 | 459 | data = struct.pack('BBBB4sIIIII',seq,0,0,0,b'SBPr', 460 | len(self.name),2048,len(self.entries),len(name_heap),self.parent_id) 461 | 462 | dir_name = self.name+b'\x0d' 463 | while len(dir_name) % 4 != 0: 464 | dir_name += b'\x00' 465 | 466 | data += dir_name 467 | 468 | for entry in self.entries: 469 | data += struct.pack("IIIIIII", 470 | entry.loadaddr, entry.execaddr, entry.length, 471 | entry.ind_disc_addr, entry.attribs, 472 | len(entry.name), 473 | heap_lookup[self.entries.index(entry)]) 474 | 475 | #check = dir_check_words(data, 7, check) 476 | 477 | data += name_heap 478 | 479 | check = dir_check_words(data, len(data)//4, 0) 480 | 481 | tail = struct.pack('4sBBB',b'oven',seq,0,0) 482 | 483 | check = dir_check_words(tail[0:4], 1, check) 484 | check = dir_check_bytes(tail[4:7], 3, check) 485 | 486 | check = (check << 0 & 0xff) ^ (check >> 8 & 0xff) ^ (check >> 16 & 0xff) ^ (check >> 24 & 0xff) 487 | 488 | while len(data) < 2040: 489 | data += b'\x00' 490 | 491 | data += tail + bytes([check]) 492 | return data 493 | 494 | def show(self): 495 | print(("Directory: {0} ({1})".format(self.name.decode('latin-1'),self.sequence))) 496 | for entry in self.entries: 497 | entry.show() 498 | 499 | def add(self, name, loadaddr, execaddr, length, attribs, ind_disc_addr): 500 | self.entries.append(BigDir.Entry(name, loadaddr, execaddr, length, attribs, ind_disc_addr)) 501 | 502 | def __getitem__(self, name): 503 | for entry in self.entries: 504 | if entry.name == name: 505 | return entry 506 | 507 | def delete(self, name): 508 | for entry in self.entries: 509 | if entry.name == name: 510 | self.entries.remove(entry) 511 | return True 512 | 513 | def find(self, name): 514 | for entry in self.entries: 515 | if entry.name.lower() == name.lower(): 516 | return entry 517 | 518 | class BootBlock(ctypes.Structure): 519 | _fields_ = [ 520 | ('defects', ctypes.c_uint32*112), 521 | ('disc_record',DiscRecord), 522 | ('non_adfs',ctypes.c_uint8*3), 523 | ('checksum',ctypes.c_uint8) ] 524 | 525 | def __init__(self, disc_record): 526 | self.defects[0] = 0x20000000 527 | self.defects[1] = 0x40000000 528 | self.disc_record = disc_record 529 | self.checksum = self.calculate_checksum() 530 | 531 | def calculate_checksum(self): 532 | checksum = 0 533 | carry = 0 534 | for b in bytes(self)[:-1]: 535 | checksum += b + carry 536 | carry = 0 537 | if checksum > 0xff: 538 | checksum &= 0xff 539 | carry = 1 540 | 541 | return checksum 542 | 543 | -------------------------------------------------------------------------------- /put_loader.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | import sys 4 | import struct 5 | from utils import get_map 6 | 7 | DOS_MAX = 128*1024*1024 8 | 9 | if len(sys.argv) != 3: 10 | print("Usage: put_loader ") 11 | exit(1) 12 | 13 | fd = open(sys.argv[2], "rb") 14 | dos_data = fd.read(DOS_MAX) 15 | fd.close() 16 | 17 | print "DOS area is {0} bytes.".format(len(dos_data)) 18 | 19 | fd = open(sys.argv[1], "r+b") 20 | 21 | fs_map = get_map(fd) 22 | lfau = fs_map.disc_record.bpmb 23 | min_frag = (fs_map.disc_record.idlen+1)*fs_map.disc_record.bpmb 24 | 25 | dos_start = min_frag / fs_map.disc_record.secsize 26 | dos_secs = len(dos_data) / fs_map.disc_record.secsize 27 | 28 | print "Disc has LFAU of {}, minium fragment size {}K.".format(lfau,min_frag/1024) 29 | print "Loader area starts at sector {}".format(dos_start) 30 | 31 | fd.seek(0, 2) 32 | disc_size = fd.tell() 33 | 34 | adfs_start = dos_start+dos_secs+1 35 | adfs_secs = disc_size / fs_map.disc_record.secsize - dos_secs 36 | 37 | fd.seek(0, 0) 38 | 39 | chs_dummy = ( 254, 255, 255 ) 40 | part_table = fd.read(512) 41 | 42 | p1 = part_table[0x1be:0x1be+16] 43 | p2 = part_table[0x1ce:0x1ce+16] 44 | p3 = part_table[0x1de:0x1de+16] 45 | p4 = part_table[0x1ee:0x1ee+16] 46 | 47 | p1_data = struct.unpack("BBBBBBBBII",p1) 48 | p4_data = struct.unpack("BBBBBBBBII",p4) 49 | 50 | p1_new = struct.pack("BBBBBBBBII", 0x80,\ 51 | chs_dummy[0],chs_dummy[1],chs_dummy[2], 11, 52 | chs_dummy[0],chs_dummy[1],chs_dummy[2], 53 | dos_start, dos_secs) 54 | 55 | p2_new = struct.pack("BBBBBBBBII", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ) 56 | p3_new = struct.pack("BBBBBBBBII", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ) 57 | 58 | p4_new = struct.pack("BBBBBBBBII", 0x00,\ 59 | chs_dummy[0],chs_dummy[1],chs_dummy[2], 0xad, 60 | chs_dummy[0],chs_dummy[1],chs_dummy[2], 61 | adfs_start, adfs_secs) 62 | 63 | new_part = part_table[0:0x1be] + p1_new + p2_new + p3_new + p4_new + "\x55\xaa" 64 | 65 | fd.seek(0, 0) 66 | fd.write(new_part) 67 | 68 | fd.seek(dos_start * fs_map.disc_record.secsize, 0) 69 | fd.write(dos_data) 70 | 71 | fd.close() 72 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from objects import Map, DiscRecord 2 | 3 | def find_map(fd): 4 | fd.seek(0xc00 + 0x1c0) 5 | disc_record = DiscRecord.from_bytes(fd.read(60)) 6 | 7 | map_address = ((disc_record.nzones // 2)*(8*disc_record.secsize-disc_record.zone_spare)-480)*disc_record.bpmb; 8 | map_length = disc_record.secsize * disc_record.nzones 9 | #map_start = map_address+64; 10 | #print("Map Address: 0x%08x, Length %x" % (map_address, map_length)) 11 | 12 | return map_address, map_length 13 | 14 | def get_map(fd): 15 | map_address, map_length = find_map(fd) 16 | # print("Map is at {}, length {}".format(map_address, map_length)) 17 | 18 | fd.seek(map_address) 19 | return Map(fd.read(map_length)) 20 | 21 | -------------------------------------------------------------------------------- /walk.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | from datetime import datetime 4 | from utils import get_map 5 | from objects import BigDir 6 | import sys 7 | 8 | if len(sys.argv) != 2: 9 | print("Usage: walk ") 10 | exit(1) 11 | 12 | def split_internal_disc_address(internal_disc_address): 13 | sector_offset = internal_disc_address & 0xff 14 | fragment_id = internal_disc_address >> 8 15 | return (fragment_id, sector_offset) 16 | 17 | def walk(directory, parent="$"): 18 | print("{}.{}".format(parent, directory.name)) 19 | for entry in directory.entries: 20 | if entry.attribs & 1<<3: 21 | frag_id, offset = split_internal_disc_address(entry.ind_disc_addr) 22 | 23 | if offset > 0: 24 | offset = (offset-1) * fs_map.disc_record.secsize 25 | 26 | if offset > 0: 27 | raise RuntimeError("Directory has sector offset > 0") 28 | 29 | locations = fs_map.find_fragment(frag_id, entry.length) 30 | data = None 31 | for start,end in locations: 32 | fd.seek(start) 33 | frag_data = fd.read(end-start) 34 | if data: 35 | data += frag_data 36 | else: 37 | data = frag_data 38 | 39 | data = data[:entry.length] 40 | 41 | walk(BigDir(data), parent+'.'+entry.name) 42 | 43 | fd = open(sys.argv[1]) 44 | fs_map = get_map(fd) 45 | 46 | root_frag_id = fs_map.disc_record.root >> 8 47 | root_sec_offset = fs_map.disc_record.root & 0xff 48 | 49 | root_locations = fs_map.find_fragment(root_frag_id, fs_map.disc_record.root_size) 50 | 51 | fd.seek((root_locations[0])[0]) 52 | 53 | root = BigDir(fd.read(fs_map.disc_record.root_size)) 54 | walk(root) 55 | 56 | --------------------------------------------------------------------------------