diff --git a/.gitmodules b/.gitmodules index 9ca2d035..d5184ca9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "isoparser"] - path = isoparser - url = https://github.com/barneygale/isoparser.git [submodule "TempleOS"] path = TempleOS url = https://github.com/minexew/TempleOS.git diff --git a/BUILDING.md b/BUILDING.md index 92da715f..db0304a1 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -10,10 +10,6 @@ During the process, the TOS ISO is first patched to enable an unattended install - use the following commands ``` -# This only needs to be done once -git submodule update --init --recursive -git apply isoparser.patch - # Some files are provided as diffs against stock TempleOS, this generates the full files cd AutoOSInstall && ./apply-patches.sh && cd .. cd Shrine && ./apply-patches.sh && cd .. @@ -21,7 +17,7 @@ cd Shrine && ./apply-patches.sh && cd .. # Finally run the machinery qemu-img create -f qcow2 ~/shrine.img 2G mkdir PkgBin -./make-dist.py TempleOSCD.ISO Shrine ~/shrine.img +./make-dist.py TOS_Distro.ISO Shrine ~/shrine.img ``` The output will be `Shrine-HEAD.iso`. diff --git a/README.md b/README.md index 0cc4c693..24ed642b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ Setting up with networking - Native Stack (highly experimental) - configure your VM networking: *Adapter Type: PCnet-PCI II* (`pcnet` in QEMU) - *Attached to: NAT* seems to be the most reliable setting, Bridged Mode also works somewhat - - Make sure the VM has **no more than 3 GB RAM**, otherwise the driver will fail to load. - On boot, Shrine will automatically attempt to acquire an IP address. If you don't get a message about "Configuring network", the adapter was not detected. - To enable tunelled networking through Snail: diff --git a/Shrine/Distro/DoDistro.diff b/Shrine/Distro/DoDistro.diff index 8cae447a..d5ea55b6 100644 --- a/Shrine/Distro/DoDistro.diff +++ b/Shrine/Distro/DoDistro.diff @@ -1,5 +1,5 @@ diff --git a/../TempleOS/TempleOSCD/Misc/DoDistro.HC b/Patched/Misc/DoDistro.HC -index 6cd1f30..9e31ba4 100644 +index 70552ac..5e0ef2a 100644 --- a/../TempleOS/TempleOSCD/Misc/DoDistro.HC +++ b/Patched/Misc/DoDistro.HC @@ -31,7 +31,7 @@ U0 MakeMyISO(U8 *_out_iso_filename) @@ -11,11 +11,11 @@ index 6cd1f30..9e31ba4 100644 CopyTree("/Linux","/Distro/Linux"); //You can leave this out. DirMk("/Distro/Tmp"); DirMk("/Distro/Tmp/ScrnShots"); -@@ -47,6 +47,6 @@ U0 MakeMyISO(U8 *_out_iso_filename) +@@ -44,6 +44,6 @@ U0 MakeMyISO(U8 *_out_iso_filename) Free(out_iso_filename); } --MakeMyISO("/Tmp/MyDistro.ISO"); -+MakeMyISO("/Tmp/ShrineDist.ISO"); +-MakeMyISO("/Tmp/MyDistro.ISO.C"); ++MakeMyISO("/Tmp/ShrineDist.ISO.C"); // Study my account examples $LK,"Cfg Strs",A="FL:::/Demo/AcctExample/TOS/TOSCfg.HC,1"$, $LK,"Update Funs",A="FL:::/Demo/AcctExample/TOS/TOSDistro.HC,1"$. diff --git a/Shrine/mkdist.script b/Shrine/mkdist.script index e2b2c761..8636d4df 100644 --- a/Shrine/mkdist.script +++ b/Shrine/mkdist.script @@ -115,7 +115,7 @@ command DelTree("/Tmp"); command DirMk("/Tmp"); command #include "/Misc/DoDistro"; -list /Tmp/ShrineDist.ISO Shrine-HEAD.iso +list /Tmp/ShrineDist.ISO.C Shrine-HEAD.iso command Reboot; wait 1 diff --git a/TempleOSCD.ISO b/TOS_Distro.ISO similarity index 90% rename from TempleOSCD.ISO rename to TOS_Distro.ISO index 77d18f4a..885d3c3e 100644 Binary files a/TempleOSCD.ISO and b/TOS_Distro.ISO differ diff --git a/TempleOS b/TempleOS index 0e587177..f75b59fc 160000 --- a/TempleOS +++ b/TempleOS @@ -1 +1 @@ -Subproject commit 0e5871772481f7a176a0f78532374bc78e24f372 +Subproject commit f75b59fc7580dce9a3fa263b45958b231e6a6476 diff --git a/inject_templeos.py b/inject_templeos.py index dbdeca64..09e4e4ac 100755 --- a/inject_templeos.py +++ b/inject_templeos.py @@ -1,28 +1,22 @@ -#!/usr/bin/env python - -from __future__ import print_function +#!/usr/bin/env python3 import sys -sys.path.append('isoparser') +sys.path.append('redseafs') -import errno -import isoparser -import os -import subprocess +from isoc import RedSea ISO_FILE = sys.argv[1] PATH_TO_REPLACE = sys.argv[2] REPLACEMENT_FILE = sys.argv[3] -iso = isoparser.parse(ISO_FILE) +iso = RedSea(ISO_FILE) with open(REPLACEMENT_FILE, 'rb') as f: data = f.read() -record = iso.record(*[s.encode() for s in PATH_TO_REPLACE.split('/')]) -patches = record.generate_patchset(data) +patches = iso.generate_patchset('/' + PATH_TO_REPLACE, data) -iso.close() +del(iso) with open(ISO_FILE, 'rb+') as isof: for offset, orig, new in patches: diff --git a/isoparser b/isoparser deleted file mode 160000 index 954c2c4a..00000000 --- a/isoparser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 954c2c4ad5a44d879d5e07d38287b3a99ae5d694 diff --git a/isoparser.patch b/isoparser.patch deleted file mode 100644 index bd19b2ad..00000000 --- a/isoparser.patch +++ /dev/null @@ -1,80 +0,0 @@ -diff --git a/isoparser/isoparser/iso.py b/isoparser/isoparser/iso.py -index adab25a..99b5027 100644 ---- a/isoparser/isoparser/iso.py -+++ b/isoparser/isoparser/iso.py -@@ -55,8 +55,9 @@ class ISO(object): - # In Rock Ridge mode, we can't use the path table - pivot = 0 - else: -- path = [part.upper() for part in path] -- pivot = len(path) -+ #path = [part.upper() for part in path] -+ #pivot = len(path) -+ pivot = 0 - - - # Resolve as much of the path as possible via the path table -diff --git a/isoparser/isoparser/record.py b/isoparser/isoparser/record.py -index 4cf19f3..5633bad 100644 ---- a/isoparser/isoparser/record.py -+++ b/isoparser/isoparser/record.py -@@ -1,11 +1,14 @@ - from . import susp, rockridge - -+import struct -+ - class Record(object): - def __init__(self, source, length, susp_starting_index=None): - self._source = source - self._content = None - target = source.cursor + length - -+ self.file_record_start = source._start * 2048 + source.cursor - _ = source.unpack('B') # TODO: extended attributes length - self.location = source.unpack_both('I') - self.length = source.unpack_both('I') -@@ -47,7 +50,7 @@ class Record(object): - - self.embedded_susp_entries = susp_entries - -- assert source.cursor <= target -+ #assert source.cursor <= target - source.unpack_raw(target - source.cursor) - - def __repr__(self): -@@ -192,5 +195,23 @@ class Record(object): - assert not self.is_directory - return self._source.get_stream(self.location, self.length) - -+ def generate_patchset(self, data): -+ #print(self.file_record_start, self.location, self.length) -+ -+ if len(data) > self.length: -+ raise Exception('File enlargement is currently not supported') -+ -+ length_offset = self.file_record_start + 9 -+ -+ length_orig = struct.pack('I', self.length) -+ length_new = struct.pack('I', len(data)) -+ -+ length_patch = (length_offset, length_orig, length_new) -+ -+ content_offset = self.location * 2048 -+ content_orig = self.content[0:len(data)] -+ content_new = data - -+ content_patch = (content_offset, content_orig, content_new) - -+ return [length_patch, content_patch] -diff --git a/isoparser/isoparser/source.py b/isoparser/isoparser/source.py -index 0ca52dc..60a170a 100644 ---- a/isoparser/isoparser/source.py -+++ b/isoparser/isoparser/source.py -@@ -178,6 +178,7 @@ class Source(object): - fetch_needed(start_sector + fetch_sectors - need_start) - - self._buff = self._buff[:length] -+ self._start = start_sector - - def save_cursor(self): - return (self._buff, self.cursor) diff --git a/redseafs/LICENSE b/redseafs/LICENSE new file mode 100644 index 00000000..cf1ab25d --- /dev/null +++ b/redseafs/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/redseafs/README.md b/redseafs/README.md new file mode 100644 index 00000000..1581a326 --- /dev/null +++ b/redseafs/README.md @@ -0,0 +1,36 @@ +# redseafs +FUSE implementation of TempleOS RedSea file system + + +This is a proof-of-concept, it will probably get better. (Time zones are not handled correctly.) + + +Currently, you can use redseafs to create/modify/read RedSea ISO.C files on any system that supports FUSE. + + +# Commands + +`isoc-mount [--rw] ` will mount an ISO.C image on `mount_point` + +Specify `--rw` to commit writes to ISO.C file, otherwise discarded on unmount. + +Specify `--2k` to pad ISO.C file to multiple of 2048 bytes, for compatibility with VirtualBox virtual CD or physical disc ONLY + +(2k padded ISO.C files will not mount with TempleOS `MountFile()`, you will get `ERROR: Not RedSea`) + +If the ISO.C file does not exist, a blank filesystem will be created (and written on unmount if `--rw` specified.) + +`fusermount -u ` to unmount + +# Installation + +Clone the repo, move `isoc-mount` and `isoc.py` to `/usr/bin`, `chmod +x`. + +On a Debian/Ubuntu system: `sudo apt install fuse; sudo apt install python-pip; sudo pip install fusepy` + +NOTE: This will install fusepy globally, if that's not what you want... then you probably don't need instructions anyway :P + +# Prerequisites + +- FUSE +- pip install: fusepy diff --git a/redseafs/isoc-mount b/redseafs/isoc-mount new file mode 100755 index 00000000..4b1c0a5d --- /dev/null +++ b/redseafs/isoc-mount @@ -0,0 +1,27 @@ +#!/usr/bin/env python +import os, sys + +pad = "0" +rw = "" +ctr = 0 +while ctr < len(sys.argv): + if sys.argv[ctr].lower() == "--rw": + rw = "rw" + del(sys.argv[ctr]) + ctr += 1 +ctr = 0 +while ctr < len(sys.argv): + if sys.argv[ctr].lower() == "--2k": + pad = "1" + del(sys.argv[ctr]) + ctr += 1 + +if len(sys.argv) < 3: + print "Usage: " + sys.argv[0] + " [--rw] [--2k] " + print " --rw: commit writes to ISO.C file, otherwise discarded on unmount." + print " If the ISO.C file does not exist, a new one will be created." + print " --2k: Pad ISO.C to multiple of 2048 bytes " + print " (for VirtualBox or physical disk only) " + sys.exit() + +os.system('"' + sys.argv[0][:sys.argv[0].rfind("/")+1] + 'isoc.py" "' + sys.argv[1] + '" "' + sys.argv[2] + '" "' + rw + '" "' + pad + '" &') diff --git a/redseafs/isoc.py b/redseafs/isoc.py new file mode 100755 index 00000000..180430dd --- /dev/null +++ b/redseafs/isoc.py @@ -0,0 +1,514 @@ +#!/usr/bin/env python +from __future__ import print_function, absolute_import, division + +import base64 +import binascii +import bz2 +import calendar +import datetime +import logging +import os +import random +import struct + +from collections import defaultdict +from errno import ENOENT +from stat import S_IFDIR, S_IFLNK, S_IFREG +from sys import argv, exit +from time import time +from time import localtime +from time import strftime + +from fuse import FUSE, FuseOSError, Operations, LoggingMixIn + +if not hasattr(__builtins__, 'bytes'): + bytes = str + +ISO9660_BOOT_BLK = bz2.decompress(base64.b64decode(""" +QlpoOTFBWSZTWf7EvQUAAFv//fREJgRSAWAALyXeECYGQAQAQBkAABAgCACAEAAACLAAuSEpAk0bS +NNBpoNHqNNHqD1NDAGmho0YjIBoANDBFJINGjQAAAABo67j0bZyRUa5yTYIQ4kKTEIUwAXoAFqrSW +4rKEBSAhABlCEiGmEFTMYxS2moWhFsmA6oWhYpKyEGJZcHEC6HB+P3rMF7qCe92GE9TJlRStvcUio +nnIx8jGJrKA+I80GdJI5JNH6AqxFBNVCiJ+/i7kinChIf2JegoA== +""")) + +epoch = datetime.datetime.utcfromtimestamp(0) + +CDATE_YEAR_DAYS_INT = 36524225 +CDIR_FILENAME_LEN = 38 +RS_ATTR_READ_ONLY = 0x01 #R +RS_ATTR_HIDDEN = 0x02 #H +RS_ATTR_SYSTEM = 0x04 #S +RS_ATTR_VOL_ID = 0x08 #V +RS_ATTR_DIR = 0x10 #D +RS_ATTR_ARCHIVE = 0x20 #A +RS_ATTR_DELETED = 0x100 #X +RS_ATTR_RESIDENT = 0x200 #T +RS_ATTR_COMPRESSED = 0x400 #Z +RS_ATTR_CONTIGUOUS = 0x800 #C +RS_ATTR_FIXED = 0x1000 #F +RS_BLK_SIZE = 512 +RS_DRV_OFFSET = 0xB000 +RS_ROOT_CLUS = 0x5A + +mon_start_days1=[ +0,31,59,90,120,151,181,212,243,273,304,334] +mon_start_days2=[ +0,31,60,91,121,152,182,213,244,274,305,335] + +def roundup(x): return x if x % 2048 == 0 else x + 2048 - x % 2048 + +def write_iso_c(self, iso_c_file, pad): + dirs = [] + dir_entries = {} + + # Create dict for each directory + for i in self.files: + if self.files[i]['st_mode'] & 40000 == 64: + dir_entries[i] = {} + dir_entries[i]['clus'] = 0 + dir_entries[i]['files'] = [] + dir_entries[i]['files'].append({'filename':'.','clus':0,'st_size':0x400,'st_mode':64,'st_mtime':time()}) + dir_entries[i]['files'].append({'filename':'..','clus':0,'st_size':0x00,'st_mode':64,'st_mtime':time()}) + dirs.append(i) + + # Place files in corresponding dicts + for d in sorted(dirs, reverse=True): + for i in self.files: + if i.find((d+"/").replace("//","/")) != -1 and i != d: + if 'filename' not in self.files[i]: + self.files[i]['filename'] = i.split('/')[len(i.split('/'))-1] + self.files[i]['clus'] = 0 + dir_entries[d]['files'].append(self.files[i]) + + # Calculate CDirEntry clusters + de_tbl_size = 0 + de_clus_ctr = RS_ROOT_CLUS + for d in sorted(dir_entries): + ct_entries = 1+len(dir_entries[d]['files']) + ct_size = RS_BLK_SIZE * int((1+(ct_entries*64) / RS_BLK_SIZE)) + de_tbl_size += ct_size + dir_entries[d]['clus'] = de_clus_ctr + de_clus_ctr += int((ct_size / RS_BLK_SIZE)) + + # Link nested CDirEntries + for d in dirs: + de_filename = d[d.rfind("/")+1:] + if len(d.split("/")) > 2: + de_parent = d[:d.rfind("/")] + else: + de_parent = "/" + de_idx = 0 + for de in dir_entries[de_parent]['files']: + if de['filename'] == de_filename: + dir_entries[de_parent]['files'][de_idx]['clus'] = dir_entries[(de_parent + "/" + de_filename).replace("//","/")]['clus'] + de_idx += 1 + + # Link dotted dirs ".", ".." + for de_parent in dir_entries: + de_idx = 0 + for de in dir_entries[de_parent]['files']: + if de['filename'] == ".": + dir_entries[de_parent]['files'][de_idx]['clus'] = dir_entries[de_parent]['clus'] + # Calculate length of this directory + dir_entries[de_parent]['files'][de_idx]['st_size'] = RS_BLK_SIZE * (1+int(64*(len(dir_entries[de_parent]['files'])) / RS_BLK_SIZE)) + if de['filename'] == "..": + dir_entries[de_parent]['files'][de_idx]['clus'] = dir_entries[("/" + de_parent[:de_parent.rfind("/")]).replace("//","/")]['clus'] + de_idx += 1 + + # Update size for subdir entries + for de_parent in dir_entries: + de_idx = 0 + for de in dir_entries[de_parent]['files']: + if de['st_mode'] & 40000 == 64 and de['filename'] != "." and de['filename'] != "..": + for sde in dir_entries[(de_parent+"/"+de['filename']).replace("//","/")]['files']: + if sde['filename'] == ".": + dir_entries[de_parent]['files'][de_idx]['st_size'] = sde['st_size'] + de_idx += 1 + + # Calculate cluster offset for files + for de_parent in dir_entries: + de_idx = 0 + for de in dir_entries[de_parent]['files']: + if de['clus'] == 0: + dir_entries[de_parent]['files'][de_idx]['clus'] = de_clus_ctr + ct_size = RS_BLK_SIZE * int((1+(de['st_size']) / RS_BLK_SIZE)) + de_clus_ctr += int((ct_size / RS_BLK_SIZE)) + de_idx += 1 + + file = open(iso_c_file, "wb") + + # Write ISO9660 boot block + file.seek(0) + file.write(ISO9660_BOOT_BLK) + + # Write CDirEntries + for d in sorted(dirs): + de_offset = int(dir_entries[d]['clus']*RS_BLK_SIZE) + for f in sorted(dir_entries[d]['files'], key=lambda k: k['filename']): + file.seek(de_offset) + if f['st_mode'] & 40000 == 64: + # Directory + file.write('\x10') + else: + # File + file.write('\x20') + if f['filename'][-2:] == ".Z": + file.write('\x0c') + else: + file.write('\x08') + file.write(f['filename'].ljust(CDIR_FILENAME_LEN,'\x00')) + + # Cluster + file.seek(de_offset+0x28) + clus_bige = binascii.unhexlify(format(int(f['clus']), '016X')) + clus_bige_ctr = 0 + while clus_bige_ctr < 8: + file.write(chr(ord(clus_bige[7-clus_bige_ctr]))) + clus_bige_ctr += 1 + + file.seek(de_offset+0x30) + # Size + size_bige = binascii.unhexlify(format(int(f['st_size']), '016X')) + size_bige_ctr = 0 + while size_bige_ctr < 8: + file.write(chr(ord(size_bige[7-size_bige_ctr]))) + size_bige_ctr += 1 + + # DateTime + file.seek(de_offset+0x38) + dt_bige = Unix2CDate(localtime(f['st_mtime']+1)) + dt_bige_ctr = 0 + while dt_bige_ctr < 8: + file.write(chr(ord(dt_bige[7-dt_bige_ctr]))) + dt_bige_ctr += 1 + + de_offset += 64 + + # Write files + for d in sorted(dirs): + de_offset = int(dir_entries[d]['clus']*RS_BLK_SIZE) + for f in sorted(dir_entries[d]['files'], key=lambda k: k['filename']): + file.seek(de_offset) + if f['st_mode'] & 40000 != 64: + file.seek(int(f['clus']*RS_BLK_SIZE)) + file.write(self.data[(d+'/'+f['filename']).replace("//","/")]) + + # If --2k, Set counter to byte multiple 2048 + if int(pad) == 1: + de_clus_ctr=roundup(de_clus_ctr) + + # Write to EOF + file.seek(RS_DRV_OFFSET+int(de_clus_ctr*RS_BLK_SIZE)-1) + file.write(chr(0)) + + # Write boot sector + + bs = [0x00]*RS_BLK_SIZE + bs[0x003] = 0x88 # signature + bs[0x008] = 0x58 # cdrom offset + + # -- cluster count + cl_sz = binascii.unhexlify(format(int(de_clus_ctr), '016X')) + cl_sz_ctr = 0 + while cl_sz_ctr < 8: + bs[0x10+cl_sz_ctr] = ord(cl_sz[7-cl_sz_ctr]) + cl_sz_ctr += 1 + bs[0x018] = 0x5A # root entry cluster [90] + bs[0x020] = 0x01 # bitmap_sects (unused for ISO.C?) + # -- unique id + id_ctr = 0 + while id_ctr<8: + bs[0x028+id_ctr] = int(random.random()*256) % 256 + id_ctr += 1 + bs[0x1FE] = 0x55 # signature + bs[0x1FF] = 0xAA # signature + + file.seek(RS_DRV_OFFSET) + for byte in bs: + file.write(chr(byte)) + file.close() + +def CDate2Unix(c_date, c_time): + year=int((c_date+1)*100000/CDATE_YEAR_DAYS_INT) + + c_year = int(year) + + i=YearStartDate(c_year) + while i > c_date: + c_year -= 1 + i=YearStartDate(c_year) + c_date -= i + + c_date = int(c_date) + if calendar.isleap(year) and c_date>29: + c_date += 1 + + k=(625*15*15*3*c_time) >> 21 + min = int(k/100/100/60 % 60) + hour = int(k/100/100/60/60) + + # lol, timezones + i_dst = 0 + if strftime("%Z") == "EDT": + chk_date = datetime.datetime(year, 1, 1, hour, min, 0) + if is_dst(chk_date): + i_dst = 1 + return (datetime.datetime(year, 1, 1, hour, min, 0)-epoch+datetime.timedelta(days=int(c_date), hours=i_dst+(-int(strftime("%z"))/100))).total_seconds() + +def is_dst(dt): + if dt.year < 2007: + # huehuehue + return False + dst_start = datetime.datetime(dt.year, 3, 8, 2, 0) + dst_start += datetime.timedelta(6 - dst_start.weekday()) + dst_end = datetime.datetime(dt.year, 11, 1, 2, 0) + dst_end += datetime.timedelta(6 - dst_end.weekday()) + return dst_start <= dt < dst_end + +def YearStartDate(year): + y1=year-1 + yd4000=y1/4000 + yd400=y1/400 + yd100=y1/100 + yd4=y1/4; + return year*365+yd4-yd100+yd400-yd4000 + +def Unix2CDate(dt): + il=YearStartDate(dt.tm_year) + i2=YearStartDate(dt.tm_year+1) + if (i2-il==365): + il += mon_start_days1[dt.tm_mon-1] + else: + il += mon_start_days2[dt.tm_mon-1] + _date = il + (dt.tm_mday-1); + _time=(100*(100*(dt.tm_sec+60*(dt.tm_min+60*dt.tm_hour)))<<21)/(15*15*3*625) + return binascii.unhexlify(format(int(_date), '08X')) + binascii.unhexlify(format(int(_time), '08X')) + +class RedSea(LoggingMixIn, Operations): + + def __init__(self, iso_c_file): + + self.files = {} + self.data = defaultdict(bytes) + self.fd = 0 + self.modified = False + self.file_details = {} + now = time() + + self.files['/'] = dict(st_mode=(S_IFDIR | 0o755), st_ctime=now, + st_mtime=now, st_atime=now, st_nlink=2) + + if os.path.exists(iso_c_file): + f = open(iso_c_file, "rb").read() + blkdev_offset = RS_DRV_OFFSET + de_list = { '': blkdev_offset + int((1+int(f[blkdev_offset+0x20]))*RS_BLK_SIZE) } + else: + de_list = {} + + # If iso_c_file exists, read dirs/files + while len(de_list) > 0 and os.path.exists(iso_c_file): + dir = next(iter(de_list.keys())) + '/' + ofs = de_list[next(iter(de_list.keys()))] + + del(de_list[next(iter(de_list.keys()))]) + # Go to first CDirEntry, skipping ".", ".." + pos = 128 + while f[ofs+pos+2] != 0x00: + ctr = int(ofs+pos) + de_attrs = u16(f[ctr:ctr+2]) + ctr += 2 + de_filename = f[ctr:ctr+CDIR_FILENAME_LEN].replace(b'\x00',b'').decode('ascii') + ctr += CDIR_FILENAME_LEN + de_clus = i64(f[ctr:ctr+8]) + ctr += 8 + de_size = i64(f[ctr:ctr+8]) + ctr += 8 + de_time = i32(f[ctr:ctr+4]) + de_date = i32(f[ctr+4:ctr+8]) + ctr += 8 + de_mode = S_IFREG | 0o755 + + if de_attrs & RS_ATTR_DIR: + de_mode = S_IFDIR | 0o755 + de_list[dir + de_filename] = ((int(de_clus))*RS_BLK_SIZE) + + self.files[dir + de_filename] = dict(st_mode=de_mode, st_ctime=CDate2Unix(de_date, de_time), + st_mtime=CDate2Unix(de_date, de_time), st_atime=now, st_nlink=2, st_size=de_size) + if not de_attrs & RS_ATTR_DIR: + self.data[dir + de_filename] = f[RS_BLK_SIZE*de_clus:(RS_BLK_SIZE*de_clus)+de_size] + self.file_details[dir + de_filename] = (int(ofs+pos), de_clus) + pos += 64 + + if os.path.exists(iso_c_file): + del(f) + + def chmod(self, path, mode): + self.files[path]['st_mode'] &= 0o770000 + self.files[path]['st_mode'] |= mode + return 0 + + def chown(self, path, uid, gid): + self.files[path]['st_uid'] = uid + self.files[path]['st_gid'] = gid + + def create(self, path, mode): + self.files[path] = dict(st_mode=(S_IFREG | mode), st_nlink=1, + st_size=0, st_ctime=time(), st_mtime=time(), + st_atime=time()) + + self.fd += 1 + self.modified = True + return self.fd + + def generate_patchset(self, path, data): + #print(self.file_record_start, self.location, self.length) + + record = self.files[path] + + if len(data) > record['st_size']: + raise Exception('File enlargement is currently not supported') + + length_offset = self.file_details[path][0] + 2 + CDIR_FILENAME_LEN + 8 + + length_orig = struct.pack('Q', record['st_size']) + length_new = struct.pack('Q', len(data)) + + length_patch = (length_offset, length_orig, length_new) + + content_offset = self.file_details[path][1] * RS_BLK_SIZE + content_orig = self.data[path][0:len(data)] + content_new = data + + content_patch = (content_offset, content_orig, content_new) + + return [length_patch, content_patch] + + def getattr(self, path, fh=None): + if path not in self.files: + raise FuseOSError(ENOENT) + return self.files[path] + + def getxattr(self, path, name, position=0): + attrs = self.files[path].get('attrs', {}) + + try: + return attrs[name] + except KeyError: + return '' # Should return ENOATTR + + def listxattr(self, path): + attrs = self.files[path].get('attrs', {}) + return attrs.keys() + + def mkdir(self, path, mode): + self.files[path] = dict(st_mode=(S_IFDIR | mode), st_nlink=2, + st_size=0, st_ctime=time(), st_mtime=time(), + st_atime=time()) + + self.files['/']['st_nlink'] += 1 + self.modified = True + + def open(self, path, flags): + self.fd += 1 + return self.fd + + def read(self, path, size, offset, fh): + return self.data[path][offset:offset + size] + + def readdir(self, path, fh): + dir = [ '.', '..' ] + if path == '/': + for f in self.files: + if f.rfind('/') == 0 and f != '/': + dir.append(str(f)[1:]) + else: + for f in self.files: + if f.startswith(path): + if f[len(path):].rfind('/') == 0 and f[len(path):] != '/': + dir.append(str(f[len(path):])[1:]) + return dir + + def readlink(self, path): + return self.data[path] + + def removexattr(self, path, name): + attrs = self.files[path].get('attrs', {}) + + try: + del attrs[name] + except KeyError: + pass # Should return ENOATTR + + def rename(self, old, new): + self.files[new] = self.files.pop(old) + self.modified = True + + def rmdir(self, path): + self.files.pop(path) + self.files['/']['st_nlink'] -= 1 + self.modified = True + + def setxattr(self, path, name, value, options, position=0): + # Ignore options + attrs = self.files[path].setdefault('attrs', {}) + attrs[name] = value + + def statfs(self, path): + size = 256 + return dict(f_bsize=RS_BLK_SIZE, f_blocks=((size*1024)*2), f_bavail=((size*1024)*2)) + + def symlink(self, target, source): + self.files[target] = dict(st_mode=(S_IFLNK | 0o777), st_nlink=1, + st_size=len(source)) + + self.data[target] = source + + def truncate(self, path, length, fh=None): + self.data[path] = self.data[path][:length] + self.files[path]['st_size'] = length + + def unlink(self, path): + self.files.pop(path) + self.modified = True + + def utimens(self, path, times=None): + now = time() + atime, mtime = times if times else (now, now) + self.files[path]['st_atime'] = atime + self.files[path]['st_mtime'] = mtime + + def write(self, path, data, offset, fh): + self.data[path] = self.data[path][:offset] + data + self.files[path]['st_size'] = len(self.data[path]) + self.modified = True + return len(data) + + def destroy(self, d): + if self.modified and argv[3] == "rw": + write_iso_c(self, argv[1], argv[4]) + +def u16(s): + u = s[0] + u += (256**1)*s[1] + return u + +def i32(s): + # Not handled correctly (yet) but idgaf + c = 1 + u = s[0] + while c < 4: + u += (256**c)*s[c] + c += 1 + return u + +def i64(s): + # Not handled correctly (yet) but idgaf + c = 1 + u = s[0] + while c < 8: + u += (256**c)*s[c] + c += 1 + return u + +if __name__ == '__main__': + fuse = FUSE(RedSea(argv[1]), argv[2], foreground=True)