From d634e6ce4c61dff6ee4450e1452b234dc9563375 Mon Sep 17 00:00:00 2001 From: ma1co Date: Thu, 11 Aug 2022 15:17:59 +0000 Subject: [PATCH] Apply tweaks by patching backup file --- pmca-gui.py | 71 ++++++----------- pmca/platform/__init__.py | 22 ++---- pmca/platform/backup.py | 151 +++++++++++++++++++++++++++++------- pmca/platform/properties.py | 4 +- pmca/platform/tweaks.py | 50 ++++++------ 5 files changed, 176 insertions(+), 122 deletions(-) diff --git a/pmca-gui.py b/pmca-gui.py index af7c96c..6155193 100755 --- a/pmca-gui.py +++ b/pmca-gui.py @@ -123,16 +123,16 @@ def start(self): def launchShell(self, backend): backend.start() + tweaks = TweakInterface(backend) - endFlag = threading.Event() - root = self.ui.master - root.run(lambda: root.after(0, lambda: TweakDialog(root, TweakInterface(backend), endFlag))) - endFlag.wait() + if next(tweaks.getTweaks(), None): + endFlag = threading.Event() + root = self.ui.master + root.run(lambda: root.after(0, lambda: TweakDialog(root, tweaks, endFlag))) + endFlag.wait() + else: + print('No tweaks available') - try: - backend.syncBackup() - except: - print('Cannot sync backup') backend.stop() def doAfter(self, result): @@ -152,42 +152,21 @@ def start(self): senserShellCommand(complete=lambda dev: self.launchShell(SenserPlatformBackend(dev))) -class TweakStatusTask(BackgroundTask): - """Task to run TweakInterface.getTweaks()""" +class TweakApplyTask(BackgroundTask): + """Task to run TweakInterface.apply()""" def doBefore(self): self.ui.setState(DISABLED) def do(self, arg): try: - return list(self.ui.tweakInterface.getTweaks()) + print('Applying tweaks...') + self.ui.tweakInterface.apply() except Exception: traceback.print_exc() def doAfter(self, result): self.ui.setState(NORMAL) - self.ui.setTweakStatus(result) - - -class TweakSetTask(BackgroundTask): - """Task to run TweakInterface.setEnabled()""" - def __init__(self, ui, id, var): - BackgroundTask.__init__(self, ui) - self.id = id - self.var = var - - def doBefore(self): - self.ui.setState(DISABLED) - return self.var.get() - - def do(self, arg): - try: - self.ui.tweakInterface.setEnabled(self.id, arg) - except Exception: - traceback.print_exc() - - def doAfter(self, result): - self.ui.setState(NORMAL) - self.ui.updateStatus() + self.ui.cancel() class InstallerUi(UiRoot): @@ -349,27 +328,25 @@ def body(self, top): self.boxFrame = Frame(tweakFrame) self.boxFrame.pack(fill=BOTH, expand=True) - self.doneButton = Button(top, text='Done', command=self.cancel, padding=5) - self.doneButton.pack(fill=X) + self.applyButton = Button(top, text='Apply', command=TweakApplyTask(self).run, padding=5) + self.applyButton.pack(fill=X) self.updateStatus() def updateStatus(self): - TweakStatusTask(self).run() - - def setTweakStatus(self, tweaks): for child in self.boxFrame.winfo_children(): child.destroy() - if tweaks: - for id, desc, status, value in tweaks: - var = IntVar(value=status) - c = Checkbutton(self.boxFrame, text=desc + '\n' + value, variable=var, command=TweakSetTask(self, id, var).run) - c.pack(fill=X) - else: - Label(self.boxFrame, text='No tweaks available').pack(fill=X) + for id, desc, status, value in self.tweakInterface.getTweaks(): + var = IntVar(value=status) + c = Checkbutton(self.boxFrame, text=desc + '\n' + value, variable=var, command=lambda id=id, var=var: self.setTweak(id, var.get())) + c.pack(fill=X) + + def setTweak(self, id, enabled): + self.tweakInterface.setEnabled(id, enabled) + self.updateStatus() def setState(self, state): - for widget in self.boxFrame.winfo_children() + [self.doneButton]: + for widget in self.boxFrame.winfo_children() + [self.applyButton]: widget.config(state=state) def cancel(self, event=None): diff --git a/pmca/platform/__init__.py b/pmca/platform/__init__.py index c3ff260..af729da 100644 --- a/pmca/platform/__init__.py +++ b/pmca/platform/__init__.py @@ -18,7 +18,6 @@ def __init__(self, backend): self.backend = backend self.addCommand('info', Command(self.info, (), 'Print device info')) - self.addCommand('tweak', Command(self.tweak, (), 'Tweak device settings')) if isinstance(self.backend, ShellPlatformBackend): self.addCommand('shell', Command(self.shell, (), 'Start an interactive shell')) @@ -37,6 +36,8 @@ def __init__(self, backend): self.addCommand('install', Command(self.install, (1,), 'Install the specified android app', '')) if isinstance(self.backend, BackupPlatformBackend): + self.addCommand('tweak', Command(self.tweak, (), 'Tweak device settings')) + bk = SubCommand() bk.addCommand('r', Command(self.readBackup, (1,), 'Read backup property', '')) bk.addCommand('w', ResidueCommand(self.writeBackup, 1, 'Write backup property', ' ')) @@ -122,14 +123,6 @@ def lockBackup(self): def unlockBackup(self): self.backend.setBackupProtection(False) - def exit(self): - if isinstance(self.backend, BackupPlatformBackend): - try: - self.backend.syncBackup() - except: - print('Cannot sync backup') - super(CameraShell, self).exit() - def tweak(self): tweakInterface = TweakInterface(self.backend) while True: @@ -146,7 +139,7 @@ def tweak(self): try: while True: try: - i = int(input('Enter number of tweak to toggle (0 to finish): ')) + i = int(input('Enter number of tweak to toggle (0 to apply): ')) if 0 <= i <= len(tweaks): break except ValueError: @@ -156,12 +149,9 @@ def tweak(self): break if i == 0: + tweakInterface.apply() break else: - try: - id, desc, status, value = tweaks[i - 1] - tweakInterface.setEnabled(id, not status) - print('Success') - except Exception as e: - print('Error: %s' % e) + id, desc, status, value = tweaks[i - 1] + tweakInterface.setEnabled(id, not status) print('') diff --git a/pmca/platform/backup.py b/pmca/platform/backup.py index 4d4a361..33c10ab 100644 --- a/pmca/platform/backup.py +++ b/pmca/platform/backup.py @@ -1,11 +1,13 @@ import abc from collections import OrderedDict +import io +from ..backup import * from ..util import * class BaseBackupProp(abc.ABC): - def __init__(self, backend, size): - self.backend = backend + def __init__(self, dataInterface, size): + self.dataInterface = dataInterface self.size = size @abc.abstractmethod @@ -18,12 +20,12 @@ def write(self, data): class BackupProp(BaseBackupProp): - def __init__(self, backend, id, size): - super(BackupProp, self).__init__(backend, size) + def __init__(self, dataInterface, id, size): + super(BackupProp, self).__init__(dataInterface, size) self.id = id def read(self): - data = self.backend.readBackup(self.id) + data = self.dataInterface.readProp(self.id) if len(data) != self.size: raise Exception('Wrong size') return data @@ -31,13 +33,13 @@ def read(self): def write(self, data): if len(data) != self.size: raise Exception('Wrong size') - self.backend.writeBackup(self.id, data) + self.dataInterface.writeProp(self.id, data) class CompoundBackupProp(BaseBackupProp): - def __init__(self, backend, props): - super(CompoundBackupProp, self).__init__(backend, sum(size for id, size in props)) - self._props = [BackupProp(backend, id, size) for id, size in props] + def __init__(self, dataInterface, props): + super(CompoundBackupProp, self).__init__(dataInterface, sum(size for id, size in props)) + self._props = [BackupProp(dataInterface, id, size) for id, size in props] def read(self): return b''.join(prop.read() for prop in self._props) @@ -50,23 +52,121 @@ def write(self, data): data = data[prop.size:] -class BackupInterface: - BACKUP_PRESET_DATA_OFFSET_VERSION = 0x0c - BACKUP_PRESET_DATA_OFFSET_ID1 = 0x28 +class BackupDataInterface(abc.ABC): + @abc.abstractmethod + def getRegion(self): + pass + + @abc.abstractmethod + def readProp(self, id): + pass + + @abc.abstractmethod + def writeProp(self, id, data): + pass + + +class BackupPlatformDataInterface(BackupDataInterface): + def __init__(self, backend): + self.backend = backend + + def getRegion(self): + return self.backend.getBackupStatus()[0x14:].decode('latin1').rstrip('\0') + + def readProp(self, id): + return self.backend.readBackup(id) + + def writeProp(self, id, data): + self.backend.writeBackup(id, data) + + +class BackupFileDataInterface(BackupDataInterface): + def __init__(self, file): + self.backup = BackupFile(file) + def getRegion(self): + return self.backup.getRegion() + + def readProp(self, id): + return self.backup.getProperty(id).data + + def writeProp(self, id, data): + self.backup.setProperty(id, data) + + def setProtection(self, enabled): + self.backup.setId1(enabled) + + def updateChecksum(self): + self.backup.updateChecksum() + +class BackupPlatformFileDataInterface(BackupFileDataInterface): def __init__(self, backend): self.backend = backend + self.file = io.BytesIO(self.backend.getBackupData()) + super(BackupPlatformFileDataInterface, self).__init__(self.file) + + def apply(self): + self.updateChecksum() + data = self.file.getvalue() + self.backend.setBackupData(data) + if self.backend.getBackupData()[0x100:] != data[0x100:]: + raise Exception('Cannot overwrite backup') + + +class BackupPatchDataInterface(BackupPlatformFileDataInterface): + def __init__(self, backend): + super(BackupPatchDataInterface, self).__init__(backend) + self.patch = {} + + def readProp(self, id): + if id in self.patch: + return self.patch[id] + return super(BackupPatchDataInterface, self).readProp(id) + + def writeProp(self, id, data): + self.patch[id] = data + + def getPatch(self): + return self.patch + + def setPatch(self, patch): + self.patch = patch + + def apply(self): + if not self.patch: + return + + patchAttr = {} + for id, data in self.patch.items(): + p = self.backup.getProperty(id) + if p.data != data and p.attr & 1: + patchAttr[id] = p.attr + self.backup.setPropertyAttr(id, p.attr & ~1) + self.backup.setProperty(id, data) + + try: + super(BackupPatchDataInterface, self).apply() + finally: + if patchAttr: + for id, attr in patchAttr.items(): + self.backup.setPropertyAttr(id, attr) + super(BackupPatchDataInterface, self).apply() + + +class BackupInterface: + def __init__(self, dataInterface): + self.dataInterface = dataInterface self._props = OrderedDict() - self.addProp('androidPlatformVersion', BackupProp(backend, 0x01660024, 8)) - self.addProp('modelCode', BackupProp(backend, 0x00e70000, 5)) - self.addProp('modelName', BackupProp(backend, 0x003e0005, 16)) - self.addProp('serialNumber', BackupProp(backend, 0x00e70003, 4)) - self.addProp('recLimit', CompoundBackupProp(backend, [(0x003c0373 + i, 1) for i in range(3)])) - self.addProp('recLimit4k', BackupProp(backend, 0x003c04b6, 2)) - self.addProp('palNtscSelector', BackupProp(backend, 0x01070148, 1)) - self.addProp('language', CompoundBackupProp(backend, [(0x010d008f + i, 1) for i in range(35)])) - self.addProp('usbAppInstaller', BackupProp(backend, 0x01640001, 1)) + self.addProp('androidPlatformVersion', BackupProp(dataInterface, 0x01660024, 8)) + self.addProp('modelCode', BackupProp(dataInterface, 0x00e70000, 5)) + self.addProp('modelName', BackupProp(dataInterface, 0x003e0005, 16)) + self.addProp('serialNumber', BackupProp(dataInterface, 0x00e70003, 4)) + self.addProp('recLimit', CompoundBackupProp(dataInterface, [(0x003c0373 + i, 1) for i in range(3)])) + self.addProp('recLimit4k', BackupProp(dataInterface, 0x003c04b6, 2)) + self.addProp('palNtscSelector', BackupProp(dataInterface, 0x01070148, 1)) + self.addProp('language', CompoundBackupProp(dataInterface, [(0x010d008f + i, 1) for i in range(35)])) + self.addProp('usbAppInstaller', BackupProp(dataInterface, 0x01640001, 1)) def addProp(self, name, prop): self._props[name] = prop @@ -78,14 +178,7 @@ def writeProp(self, name, data): return self._props[name].write(data) def getRegion(self): - return self.backend.getBackupStatus()[0x14:].decode('latin1').rstrip('\0') - - def getProtection(self): - data = self.backend.getBackupData() - version = data[self.BACKUP_PRESET_DATA_OFFSET_VERSION:self.BACKUP_PRESET_DATA_OFFSET_VERSION+4] - if version not in [b'BK%d\0' % i for i in [2, 3, 4]]: - raise Exception('Unsupported backup version') - return parse32le(data[self.BACKUP_PRESET_DATA_OFFSET_ID1:self.BACKUP_PRESET_DATA_OFFSET_ID1+4]) != 0 + return self.dataInterface.getRegion() def getDefaultLanguages(self, region): return { diff --git a/pmca/platform/properties.py b/pmca/platform/properties.py index 1a9d553..6469df0 100644 --- a/pmca/platform/properties.py +++ b/pmca/platform/properties.py @@ -17,7 +17,7 @@ def get(self): class BackupProperty(Property): def __init__(self, backend, name): super(BackupProperty, self).__init__(backend) - self._backup = BackupInterface(backend) + self._backup = BackupInterface(BackupPlatformDataInterface(backend)) self.name = name def _read(self): @@ -37,7 +37,7 @@ def get(self): class BackupRegionProperty(Property): def __init__(self, backend): super(BackupRegionProperty, self).__init__(backend) - self._backup = BackupInterface(backend) + self._backup = BackupInterface(BackupPlatformDataInterface(backend)) def get(self): return self._backup.getRegion() diff --git a/pmca/platform/tweaks.py b/pmca/platform/tweaks.py index aba2156..a97a205 100644 --- a/pmca/platform/tweaks.py +++ b/pmca/platform/tweaks.py @@ -5,9 +5,6 @@ from ..util import * class BaseTweak(abc.ABC): - def __init__(self, backend): - self.backend = backend - def available(self): try: self.enabled() @@ -28,9 +25,8 @@ def strValue(self): class BackupTweak(BaseTweak): - def __init__(self, backend, name, checkValue): - super(BackupTweak, self).__init__(backend) - self._backup = BackupInterface(backend) + def __init__(self, backup, name, checkValue): + self._backup = backup self._name = name self._checkValue = checkValue @@ -139,32 +135,23 @@ def strValue(self): return '%d / %d languages activated' % (sum(1 if l == self.BACKUP_LANG_ENABLED else 0 for l in val), len(val)) -class ProtectionTweak(BaseTweak): - def __init__(self, backend): - super(ProtectionTweak, self).__init__(backend) - self._backup = BackupInterface(self.backend) - - def enabled(self): - return not self._backup.getProtection() - - def setEnabled(self, enabled): - self.backend.setBackupProtection(not enabled) - - def strValue(self): - return 'Protection disabled' if self.enabled() else 'Protection enabled' - - class TweakInterface: def __init__(self, backend): + self.backend = backend self._tweaks = OrderedDict() - if isinstance(backend, BackupPlatformBackend): - self.addTweak('recLimit', 'Disable video recording limit', RecLimitTweak(backend)) - self.addTweak('recLimit4k', 'Disable 4K video recording limit', RecLimit4kTweak(backend)) - self.addTweak('language', 'Unlock all languages', LanguageTweak(backend)) - self.addTweak('palNtscSelector', 'Enable PAL / NTSC selector & warning', BooleanBackupTweak(backend, 'palNtscSelector')) - self.addTweak('usbAppInstaller', 'Enable USB app installer', BooleanBackupTweak(backend, 'usbAppInstaller')) - self.addTweak('protection', 'Unlock protected settings', ProtectionTweak(backend)) + try: + self.backupPatch = BackupPatchDataInterface(self.backend) + except: + self.backupPatch = None + return + + backup = BackupInterface(self.backupPatch) + self.addTweak('recLimit', 'Disable video recording limit', RecLimitTweak(backup)) + self.addTweak('recLimit4k', 'Disable 4K video recording limit', RecLimit4kTweak(backup)) + self.addTweak('language', 'Unlock all languages', LanguageTweak(backup)) + self.addTweak('palNtscSelector', 'Enable PAL / NTSC selector & warning', BooleanBackupTweak(backup, 'palNtscSelector')) + self.addTweak('usbAppInstaller', 'Enable USB app installer', BooleanBackupTweak(backup, 'usbAppInstaller')) def addTweak(self, name, desc, tweak): self._tweaks[name] = (desc, tweak) @@ -185,3 +172,10 @@ def getTweaks(self): for name, (desc, tweak) in self._tweaks.items(): if tweak.available(): yield name, desc, tweak.enabled(), tweak.strValue() + + def apply(self): + if self.backupPatch: + patch = BackupPatchDataInterface(self.backend) + patch.setPatch(self.backupPatch.getPatch()) + patch.setProtection(True) + patch.apply()