diff --git a/ProperTree.bat b/ProperTree.bat new file mode 100644 index 0000000..3140e9d --- /dev/null +++ b/ProperTree.bat @@ -0,0 +1,45 @@ +@echo off +setlocal enableDelayedExpansion + +REM Setup initial vars +set "script_name=%~n0.command" +set "thisDir=%~dp0" + +REM Check for our script first +if not exist "!thisDir!\!script_name!" ( + echo Could not find !script_name!. + echo Please make sure to run this script from the same directory + echo as !script_name!. + echo. + echo Press [enter] to quit. + pause > nul + exit /b +) + +REM Get python location +FOR /F "tokens=* USEBACKQ" %%F IN (`where python 2^> nul`) DO ( + SET "python=%%F" +) + +REM Check for py and give helpful hints! +if /i "!python!"=="" ( + echo Python is not installed or not found in your PATH var. + echo Please install it from https://www.python.org/downloads/windows/ + echo. + echo Make sure you check the box labeled: + echo. + echo "Add Python X.X to PATH" + echo. + echo Where X.X is the py version you're installing. + echo. + echo Press [enter] to quit. + pause > nul + exit /b +) + +REM Python found +if "%*"=="" ( + "!python!" "!thisDir!!script_name!" +) else ( + "!python!" "!thisDir!!script_name!" %* +) \ No newline at end of file diff --git a/ProperTree.command b/ProperTree.command new file mode 100644 index 0000000..43eb903 --- /dev/null +++ b/ProperTree.command @@ -0,0 +1,364 @@ +#!/usr/bin/env python +import sys, os, binascii, base64 +try: + import Tkinter as tk + import ttk + import tkFileDialog as fd + import tkMessageBox as mb +except: + import tkinter as tk + import tkinter.ttk as ttk + from tkinter import filedialog as fd + from tkinter import messagebox as mb +from Scripts import * + +class ProperTree: + def __init__(self): + # Create the new tk object + self.tk = tk.Tk() + self.tk.title("Convert Values") + self.tk.minsize(width=640,height=130) + self.tk.resizable(False, False) + # self.tk.columnconfigure(2,weight=1) + # Build the Hex <--> Base64 converter + f_label = tk.Label(self.tk, text="From:") + f_label.grid(row=0,column=0) + t_label = tk.Label(self.tk, text="To:") + t_label.grid(row=1,column=0) + # Setup the from/to option menus + f_title = tk.StringVar(self.tk) + t_title = tk.StringVar(self.tk) + f_title.set("Base64") + t_title.set("Hex") + f_option = tk.OptionMenu(self.tk, f_title, "Ascii", "Base64", "Hex", command=self.change_from_type) + t_option = tk.OptionMenu(self.tk, t_title, "Ascii", "Base64", "Hex", command=self.change_to_type) + self.from_type = "Base64" + self.to_type = "Hex" + f_option.grid(row=0,column=1,sticky="we") + t_option.grid(row=1,column=1,sticky="we") + + self.f_text = tk.Entry(self.tk,width=80) + self.f_text.delete(0,tk.END) + self.f_text.insert(0,"") + self.f_text.grid(row=0,column=2,sticky="we",padx=10,pady=10) + + self.t_text = tk.Entry(self.tk,width=80) + self.t_text.configure(state='normal') + self.t_text.delete(0,tk.END) + self.t_text.insert(0,"") + self.t_text.configure(state='readonly') + self.t_text.grid(row=1,column=2,sticky="we",padx=10,pady=10) + + self.c_button = tk.Button(self.tk, text="Convert", command=self.convert_values) + self.c_button.grid(row=2,column=2,sticky="e",padx=10,pady=10) + + self.tk.bind("", self.convert_values) + self.tk.bind("", self.convert_values) + + self.clipboard = None + + # Setup the menu-related keybinds - and change the app name if needed + key="Control" + sign = "Ctr+" + if str(sys.platform) == "darwin": + # Remap the quit function to our own + self.tk.createcommand('::tk::mac::Quit', self.quit) + # Import the needed modules to change the bundle name and force focus + try: + from Foundation import NSBundle + from Cocoa import NSRunningApplication, NSApplicationActivateIgnoringOtherApps + app = NSRunningApplication.runningApplicationWithProcessIdentifier_(os.getpid()) + app.activateWithOptions_(NSApplicationActivateIgnoringOtherApps) + bundle = NSBundle.mainBundle() + if bundle: + info = bundle.localizedInfoDictionary() or bundle.infoDictionary() + if info and info['CFBundleName'] == 'Python': + info['CFBundleName'] = "ProperTree" + except: + pass + key="Command" + sign=key+"+" + + self.tk.protocol("WM_DELETE_WINDOW", self.close_window) + # Close initial window + self.close_window(None,False) + + # Setup the top level menu + file_menu = tk.Menu(self.tk) + main_menu = tk.Menu(self.tk) + main_menu.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="New ({}N)".format(sign), command=self.new_plist) + file_menu.add_command(label="Open ({}O)".format(sign), command=self.open_plist) + file_menu.add_command(label="Save ({}S)".format(sign), command=self.save_plist) + file_menu.add_command(label="Save As ({}Shift+S)".format(sign), command=self.save_plist_as) + file_menu.add_command(label="Duplicate ({}D)".format(sign), command=self.duplicate_plist) + file_menu.add_separator() + file_menu.add_command(label="Convert Window ({}T)".format(sign), command=self.show_convert) + file_menu.add_command(label="Strip Comments ({}M)".format(sign), command=self.strip_comments) + file_menu.add_separator() + file_menu.add_command(label="View Data As Hex", command=lambda:self.change_data_display("hex")) + file_menu.add_command(label="View Data As Base64", command=lambda:self.change_data_display("base64")) + if not str(sys.platform) == "darwin": + file_menu.add_separator() + file_menu.add_command(label="Quit ({}Q)".format(sign), command=self.quit) + self.tk.config(menu=main_menu) + + # Set bindings + self.tk.bind_all("<{}-n>".format(key), self.new_plist) + self.tk.bind_all("<{}-o>".format(key), self.open_plist) + self.tk.bind_all("<{}-s>".format(key), self.save_plist) + self.tk.bind_all("<{}-S>".format(key), self.save_plist_as) + self.tk.bind_all("<{}-d>".format(key), self.duplicate_plist) + self.tk.bind_all("<{}-c>".format(key), self.copy_selection) + self.tk.bind_all("<{}-v>".format(key), self.paste_selection) + self.tk.bind_all("<{}-t>".format(key), self.show_convert) + self.tk.bind_all("<{}-z>".format(key), self.undo) + self.tk.bind_all("<{}-Z>".format(key), self.redo) + self.tk.bind_all("<{}-m>".format(key), self.strip_comments) + if not str(sys.platform) == "darwin": + # Rewrite the default Command-Q command + self.tk.bind_all("<{}-q>".format(key), self.quit) + + # create a fresh plist to start + self.start_window = self.new_plist() + + # Start our run loop + tk.mainloop() + + def change_data_display(self, new_data = None): + windows = self.stackorder(self.tk) + if not len(windows): + # Nothing to save + return + window = windows[-1] # Get the last item (most recent) + if window == self.tk: + return + window.change_data_display(new_data) + + def close_window(self, event = None, check_close = True): + # Remove the default window that comes from it + if str(sys.platform) == "darwin": + self.tk.iconify() + else: + self.tk.withdraw() + if check_close: + windows = self.stackorder(self.tk) + if not len(windows): + # Quit if all windows are closed + self.quit() + + def strip_comments(self, event = None): + windows = self.stackorder(self.tk) + if not len(windows): + # Nothing to save + return + window = windows[-1] # Get the last item (most recent) + if window == self.tk: + return + window.strip_comments(event) + + def change_to_type(self, value): + self.to_type = value + self.convert_values() + + def change_from_type(self, value): + self.from_type = value + + def show_convert(self, event = None): + self.tk.deiconify() + + def convert_values(self, event = None): + from_value = self.f_text.get() + if not len(from_value): + # Empty - nothing to convert + return + # Pre-check for hex potential issues + if self.from_type.lower() == "hex": + if from_value.lower().startswith("0x"): + from_value = from_value[2:] + from_value = from_value.replace(" ","") + if [x for x in from_value if x.lower() not in "0123456789abcdef"]: + self.tk.bell() + if not mb.showerror("Invalid Hex Data","Invalid character in passed hex data.",parent=self.tk): + return + try: + # Handle the from data + if sys.version_info >= (3,0): + # Convert to bytes + from_value = from_value.encode("utf-8") + if self.from_type.lower() == "base64": + from_value = base64.b64decode(from_value) + elif self.from_type.lower() == "hex": + from_value = binascii.unhexlify(from_value) + # Let's get the data converted + to_value = from_value + if self.to_type.lower() == "base64": + to_value = base64.b64encode(from_value) + elif self.to_type.lower() == "hex": + to_value = binascii.hexlify(from_value) + if sys.version_info >= (3,0): + # Convert to bytes + to_value = to_value.decode("utf-8") + if self.to_type.lower() == "hex": + # Capitalize it, and pad with spaces + to_value = "{}".format(" ".join((to_value[0+i:8+i] for i in range(0, len(to_value), 8))).upper()) + # Set the text box + self.t_text.configure(state='normal') + self.t_text.delete(0,tk.END) + self.t_text.insert(0,to_value) + self.t_text.configure(state='readonly') + except Exception as e: + self.tk.bell() + mb.showerror("Conversion Error",str(e),parent=self.tk) + + ### ### + # Save/Load Plist Functions # + ### ### + + def copy_selection(self, event = None): + windows = self.stackorder(self.tk) + if not len(windows): + # Nothing to save + return + window = windows[-1] # Get the last item (most recent) + if window == self.tk: + return + node = window._tree.focus() + if node == "": + # Nothing to copy + return + self.clipboard = window.nodes_to_values(node,{}) + + def paste_selection(self, event = None): + if self.clipboard == None: + return + windows = self.stackorder(self.tk) + if not len(windows): + # Nothing to save + return + window = windows[-1] # Get the last item (most recent) + if window == self.tk: + return + window.paste_selection(self.clipboard) + + def duplicate_plist(self, event = None): + windows = self.stackorder(self.tk) + if not len(windows): + # Nothing to save + return + window = windows[-1] # Get the last item (most recent) + if window == self.tk: + return + plist_data = window.nodes_to_values() + plistwindow.PlistWindow(self, self.tk).open_plist(None,plist_data) + + def save_plist(self, event = None): + windows = self.stackorder(self.tk) + if not len(windows): + # Nothing to save + return + window = windows[-1] # Get the last item (most recent) + if window == self.tk: + return + window.save_plist(event) + + def save_plist_as(self, event = None): + windows = self.stackorder(self.tk) + if not len(windows): + # Nothing to save + return + window = windows[-1] # Get the last item (most recent) + if window == self.tk: + return + window.save_plist_as(event) + + def undo(self, event = None): + windows = self.stackorder(self.tk) + if not len(windows): + # Nothing to save + return + window = windows[-1] # Get the last item (most recent) + if window == self.tk: + return + window.reundo(event) + + def redo(self, event = None): + windows = self.stackorder(self.tk) + if not len(windows): + # Nothing to save + return + window = windows[-1] # Get the last item (most recent) + if window == self.tk: + return + window.reundo(event,False) + + def new_plist(self, event = None): + # Creates a new plistwindow object + window = plistwindow.PlistWindow(self, self.tk) + window.focus_force() + window.update() + return window + + def open_plist(self, event=None): + # Prompt the user to open a plist, attempt to load it, and if successful, + # set its path as our current_plist value + current_window = None + windows = self.stackorder(self.tk) + if len(windows) == 1 and windows[0] == self.start_window and windows[0].edited == False and windows[0].current_plist == None: + # Fresh window - replace the contents + current_window = windows[0] + path = fd.askopenfilename(title = "Select config.plist",filetypes=[("Plist files", "*.plist")],parent=current_window) + if not len(path): + # User cancelled - bail + return None + # Verify that no other window has that file selected already + for window in windows: + if window == self.tk: + continue + if window.current_plist == path: + # found one - just make this focus instead + window.focus_force() + window.update() + window.bell() + mb.showerror("File Already Open", "{} is already open here.".format(path), parent=window) + return + # Let's try to load the plist + try: + with open(path,"rb") as f: + plist_data = plist.load(f) + except Exception as e: + # Had an issue, throw up a display box + # print("{}\a".format(str(e))) + self.tk.bell() + mb.showerror("An Error Occurred While Opening {}".format(os.path.basename(path)), str(e),parent=current_window) + return None + else: + # Opened it correctly - let's load it, and set our values + if current_window: + current_window.open_plist(path,plist_data) + else: + # Need to create one first + plistwindow.PlistWindow(self, self.tk).open_plist(path,plist_data) + return True + + def stackorder(self, root): + """return a list of root and toplevel windows in stacking order (topmost is last)""" + c = root.children + s = root.tk.eval('wm stackorder {}'.format(root)) + L = [x.lstrip('.') for x in s.split()] + return [(c[x] if x else root) for x in L] + + def quit(self, event=None): + # Check if we need to save first, then quit if we didn't cancel + for window in self.stackorder(self.tk)[::-1]: + if window == self.tk: + continue + if window.check_save() == None: + # User cancelled or we failed to save, bail + return + window.destroy() + # Actually quit the tkinter session + self.tk.destroy() + +if __name__ == '__main__': + p = ProperTree() \ No newline at end of file diff --git a/Scripts/__init__.py b/Scripts/__init__.py new file mode 100644 index 0000000..962c1d3 --- /dev/null +++ b/Scripts/__init__.py @@ -0,0 +1,4 @@ +from os.path import dirname, basename, isfile +import glob +modules = glob.glob(dirname(__file__)+"/*.py") +__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] \ No newline at end of file diff --git a/Scripts/menu.plist b/Scripts/menu.plist new file mode 100644 index 0000000..6df87a8 --- /dev/null +++ b/Scripts/menu.plist @@ -0,0 +1,550 @@ + + + + + Clover + + ACPI/DSDT/Patches + + + name + New Blank Entry + types + d/d/a + value + + Comment + + Disabled + + Find + + + Replace + + + + + + + OpenCore + + ACPI/Add + + + name + New Blank Entry + types + d/a + value + + Comment + + Enabled + + Path + + + + + ACPI/Block + + + name + New Blank Entry + types + d/a + value + + All + + Comment + + Enabled + + OemTableId + + + Path + + TableLength + 0 + TableSignature + + + + + + name + Drop DMAR + types + d/a + value + + All + + Comment + Drop DMAR + Enabled + + OemTableId + + + Path + + TableLength + 0 + TableSignature + + RE1BUg== + + + + + Kernel/Add + + + name + Add Lilu kext + types + s/a + value + + BundlePath + Lilu.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/Lilu + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add VirtualSmc kext + types + s/a + value + + BundlePath + VirtualSMC.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/VirtualSMC + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add WhateverGreen kext + types + s/a + value + + BundlePath + WhateverGreen.kext + Comment + Video card + Enabled + + ExecutablePath + Contents/MacOS/WhateverGreen + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add AppleALC kext + types + s/a + value + + BundlePath + AppleALC.kext + Comment + Sound + Enabled + + ExecutablePath + Contents/MacOS/AppleALC + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add USBInjectAll kext + types + s/a + value + + BundlePath + USBInjectAll.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/USBInjectAll + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add VoodooPS2Controller [first to load] + types + s/a + value + + BundlePath + VoodooPS2Controller.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/VoodooPS2Controller + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add VoodooPS2Keyboard [Load after VPS2C] + types + s/a + value + + BundlePath + VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/VoodooPS2Keyboard + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add VoodooPS2Trackpad [Load after VPS2C] + types + s/a + value + + BundlePath + VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Trackpad.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/VoodooPS2Trackpad + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add AirportBrcmFixup kext + types + s/a + value + + BundlePath + AirportBrcmFixup.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/AirportBrcmFixup + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add CPUFriend kext + types + s/a + value + + BundlePath + CPUFriend.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/CPUFriend + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add CPUFriend DataProvider [load after CPUFriend] + types + s/a + value + + BundlePath + CPUFriendDataProvider.kext + Comment + + Enabled + + ExecutablePath + + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + VoodooI2C: Services [First to load] + types + s/a + value + + BundlePath + VoodooI2C.kext/Contents/PlugIns/VoodooI2CServices.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/VoodooI2CServices + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + VoodooI2C: GPIO [Second to load] + types + s/a + value + + BundlePath + VoodooI2C.kext/Contents/PlugIns/VoodooGPIO.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/VoodooGPIO + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + VoodooI2C: Main [Third to load] + types + s/a + value + + BundlePath + VoodooI2C.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/VoodooI2C + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + VoodooI2C: HID [Last to load] + types + s/a + value + + BundlePath + VoodooI2CHID.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/VoodooI2CHID + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add NoTouchID kext + types + s/a + value + + BundlePath + NoTouchID.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/NoTouchID + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add BrcmFirmwareData kext + types + s/a + value + + BundlePath + BrcmFirmwareData.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/BrcmFirmwareData + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add BrcmPatchRAM2 kext + types + s/a + value + + BundlePath + BrcmPatchRAM2.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/BrcmPatchRAM2 + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add SMCBatteryManager kext [Load after VSMC] + types + s/a + value + + BundlePath + SMCBatteryManager.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/SMCBatteryManager + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add SMCProcessor kext [load after VSMC] + types + s/a + value + + BundlePath + SMCProcessor.kext + Comment + + Enabled + + ExecutablePath + Contents/MacOS/SMCProcessor + MatchKernel + + PlistPath + Contents/Info.plist + + + + name + Add New Blank Entry + types + s/a + value + + BundlePath + + Comment + + Enabled + + ExecutablePath + + MatchKernel + + PlistPath + Contents/Info.plist + + + + + + diff --git a/Scripts/plist.py b/Scripts/plist.py new file mode 100644 index 0000000..7c4f7d5 --- /dev/null +++ b/Scripts/plist.py @@ -0,0 +1,215 @@ +### ### +# Imports # +### ### + +import datetime +from io import BytesIO +import os +import plistlib +import struct +import sys + +if sys.version_info < (3,0): + # Force use of StringIO instead of cStringIO as the latter + # has issues with Unicode strings + from StringIO import StringIO + +try: + FMT_XML = plistlib.FMT_XML +except: + FMT_XML = None + +### ### +# Helper Methods # +### ### + +def _check_py3(): + return True if sys.version_info >= (3, 0) else False + +def _is_binary(fp): + if isinstance(fp, _get_inst()): + return fp.startswith(b"bplist00") + header = fp.read(32) + fp.seek(0) + return header[:8] == b'bplist00' + +def _get_inst(): + if _check_py3(): + return (str) + else: + return (str, unicode) + +### ### +# Deprecated Functions - Remapped # +### ### + +def readPlist(pathOrFile): + if not isinstance(pathOrFile, _get_inst()): + return load(pathOrFile) + with open(pathOrFile, "rb") as f: + return load(f) + +def writePlist(value, pathOrFile): + if not isinstance(pathOrFile, _get_inst()): + return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False) + with open(pathOrFile, "wb") as f: + return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False) + +### ### +# Remapped Functions # +### ### + +def load(fp, fmt=None, use_builtin_types=True, dict_type=dict): + if _check_py3(): + return plistlib.load(fp, fmt=fmt, use_builtin_types=use_builtin_types, dict_type=dict_type) + elif not _is_binary(fp): + return plistlib.readPlist(fp) + else: + return readBinaryPlistFile(fp) + +def loads(value, fmt=None, use_builtin_types=True, dict_type=dict): + if _check_py3(): + # Requires fp to be a BytesIO wrapper around a bytes object + if isinstance(value, _get_inst()): + # If it's a string - encode it + value = value.encode() + # Load it + return plistlib.load(BytesIO(value), fmt=fmt, use_builtin_types=use_builtin_types, dict_type=dict_type) + else: + if _is_binary(value): + # Has the proper header to be a binary plist + return readBinaryPlistFile(BytesIO(value)) + else: + # Is not binary - assume a string - and try to load + # We avoid using readPlistFromString() as that uses + # cStringIO and fails when Unicode strings are detected + # Don't subclass - keep the parser local + from xml.parsers.expat import ParserCreate + # Create a new PlistParser object - then we need to set up + # the values and parse. + p = plistlib.PlistParser() + parser = ParserCreate() + parser.StartElementHandler = p.handleBeginElement + parser.EndElementHandler = p.handleEndElement + parser.CharacterDataHandler = p.handleData + if isinstance(value, unicode): + # Encode unicode -> string; use utf-8 for safety + value = value.encode("utf-8") + # Parse the string + parser.Parse(value, 1) + return p.root + + rootObject = p.parse(s) + return rootObject + +def dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False): + if _check_py3(): + plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) + else: + plistlib.writePlist(value, fp) + +def dumps(value, fmt=FMT_XML, skipkeys=False): + if _check_py3(): + return plistlib.dumps(value, fmt=fmt, skipkeys=skipkeys).decode("utf-8") + else: + # We avoid using writePlistToString() as that uses + # cStringIO and fails when Unicode strings are detected + f = StringIO() + plistlib.writePlist(value, f) + return f.getvalue() + +### ### +# Binary Plist Stuff For Py2 # +### ### + +# timestamp 0 of binary plists corresponds to 1/1/2001 (year of Mac OS X 10.0), instead of 1/1/1970. +MAC_OS_X_TIME_OFFSET = (31 * 365 + 8) * 86400 + +class InvalidFileException(ValueError): + def __str__(self): + return "Invalid file" + def __unicode__(self): + return "Invalid file" + +def readBinaryPlistFile(in_file): + """ + Read a binary plist file, following the description of the binary format: http://opensource.apple.com/source/CF/CF-550/CFBinaryPList.c + Raise InvalidFileException in case of error, otherwise return the root object, as usual + + Original patch diffed here: https://bugs.python.org/issue14455 + """ + in_file.seek(-32, os.SEEK_END) + trailer = in_file.read(32) + if len(trailer) != 32: + return InvalidFileException() + offset_size, ref_size, num_objects, top_object, offset_table_offset = struct.unpack('>6xBB4xL4xL4xL', trailer) + in_file.seek(offset_table_offset) + object_offsets = [] + offset_format = '>' + {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[offset_size] * num_objects + ref_format = {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[ref_size] + int_format = {0: (1, '>B'), 1: (2, '>H'), 2: (4, '>L'), 3: (8, '>Q'), } + object_offsets = struct.unpack(offset_format, in_file.read(offset_size * num_objects)) + def getSize(token_l): + """ return the size of the next object.""" + if token_l == 0xF: + m = ord(in_file.read(1)) & 0x3 + s, f = int_format[m] + return struct.unpack(f, in_file.read(s))[0] + return token_l + def readNextObject(offset): + """ read the object at offset. May recursively read sub-objects (content of an array/dict/set) """ + in_file.seek(offset) + token = in_file.read(1) + token_h, token_l = ord(token) & 0xF0, ord(token) & 0x0F #high and low parts + if token == '\x00': + return None + elif token == '\x08': + return False + elif token == '\x09': + return True + elif token == '\x0f': + return '' + elif token_h == 0x10: #int + result = 0 + for k in xrange((2 << token_l) - 1): + result = (result << 8) + ord(in_file.read(1)) + return result + elif token_h == 0x20: #real + if token_l == 2: + return struct.unpack('>f', in_file.read(4))[0] + elif token_l == 3: + return struct.unpack('>d', in_file.read(8))[0] + elif token_h == 0x30: #date + f = struct.unpack('>d', in_file.read(8))[0] + return datetime.datetime.utcfromtimestamp(f + MAC_OS_X_TIME_OFFSET) + elif token_h == 0x40: #data + s = getSize(token_l) + return plistlib.Data(in_file.read(s)) + elif token_h == 0x50: #ascii string + s = getSize(token_l) + return in_file.read(s) + elif token_h == 0x60: #unicode string + s = getSize(token_l) + return in_file.read(s * 2).decode('utf-16be') + elif token_h == 0x80: #uid + return in_file.read(token_l + 1) + elif token_h == 0xA0: #array + s = getSize(token_l) + obj_refs = struct.unpack('>' + ref_format * s, in_file.read(s * ref_size)) + return map(lambda x: readNextObject(object_offsets[x]), obj_refs) + elif token_h == 0xC0: #set + s = getSize(token_l) + obj_refs = struct.unpack('>' + ref_format * s, in_file.read(s * ref_size)) + return set(map(lambda x: readNextObject(object_offsets[x]), obj_refs)) + elif token_h == 0xD0: #dict + result = {} + s = getSize(token_l) + key_refs = struct.unpack('>' + ref_format * s, in_file.read(s * ref_size)) + obj_refs = struct.unpack('>' + ref_format * s, in_file.read(s * ref_size)) + for k, o in zip(key_refs, obj_refs): + key = readNextObject(object_offsets[k]) + obj = readNextObject(object_offsets[o]) + result[key] = obj + return result + raise InvalidFileException() + return readNextObject(object_offsets[top_object]) diff --git a/Scripts/plistwindow.py b/Scripts/plistwindow.py new file mode 100644 index 0000000..f379549 --- /dev/null +++ b/Scripts/plistwindow.py @@ -0,0 +1,1435 @@ +#!/usr/bin/env python +import sys, os, plistlib, base64, binascii, datetime, tempfile, shutil, re, itertools, math +try: + # Python 2 + import Tkinter as tk + import ttk + import tkFileDialog as fd + import tkMessageBox as mb + from itertools import izip_longest as izip +except: + # Python 3 + import tkinter as tk + import tkinter.ttk as ttk + from tkinter import filedialog as fd + from tkinter import messagebox as mb + from itertools import zip_longest as izip +sys.path.append(os.path.abspath(os.path.dirname(os.path.realpath(__file__)))) +import plist + +class EntryPopup(tk.Entry): + def __init__(self, parent, text, cell, column, **kw): + tk.Entry.__init__(self, parent, **kw) + + self.insert(0, text) + self.select_all() + self['state'] = 'normal' + self['readonlybackground'] = 'white' + self['selectbackground'] = '#1BA1E2' + self['exportselection'] = True + + self.cell = cell + self.column = column + self.parent = parent + self.master = parent._nametowidget(parent.winfo_parent()) + + self.focus_force() + + if str(sys.platform) == "darwin": + self.bind("", self.select_all) + self.bind("", self.copy) + self.bind("", self.paste) + else: + self.bind("", self.select_all) + self.bind("", self.copy) + self.bind("", self.paste) + self.bind("", lambda *ignore: self.destroy()) + self.bind("", self.confirm) + self.bind("", self.confirm) + self.bind("", self.goto_start) + self.bind("", self.goto_end) + + def goto_start(self, event): + self.selection_range(0, 0) + self.icursor(0) + return 'break' + + def goto_end(self, event): + self.selection_range(0, 0) + self.icursor(len(self.get())) + return 'break' + + def copy(self, event): + try: + get = self.selection_get() + except: + get = "" + if not len(get): + return 'break' + self.clipboard_clear() + self.clipboard_append(get) + self.update() + return 'break' + + def paste(self, event): + contents = self.clipboard_get() + if len(contents): + try: + get = self.selection_get() + except: + get = "" + if len(get): + # Have a selection - let's get the first and last + start = self.index(tk.SEL_FIRST) + end = self.index(tk.SEL_LAST) + self.delete(start,end) + else: + start = self.index(tk.INSERT) + self.insert(start,contents) + return 'break' + + def select_all(self, *ignore): + self.selection_range(0, 'end') + # returns 'break' to interrupt default key-bindings + return 'break' + + def confirm(self, event): + if self.column == "#0": + # First we make sure that no other siblings + # have the same name - as dict names need to be + # unique + parent = self.parent.parent(self.cell) + text = self.get() + for child in self.parent.get_children(parent): + if child == self.cell: + # Skip ourselves + continue + # Check if our text is equal to any other + # keys + if text == self.parent.item(child, "text"): + # Have a match, beep and bail + if not event == None: + self.bell() + if not mb.askyesno("Invalid Key Name","That key name already exists in that dict.\n\nWould you like to keep editing?",parent=self.parent): + self.destroy() + # print("Key names must be unique!\a") + return + # Add to undo stack + self.master.add_undo({"cell":self.cell,"name":self.parent.item(self.cell,"text")}) + # No matches, should be safe to set + self.parent.item(self.cell, text=self.get()) + else: + # Need to walk the values and pad + values = self.parent.item(self.cell)["values"] + values = [] if values == "" else values + # Count up, padding as we need + index = int(self.column.replace("#","")) + values += [''] * (index - len(values)) + + original = [x for x in values] + + # Sanitize our value based on type + type_value = self.master.get_check_type(self.cell).lower() + value = self.get() + # We need to sanitize data and numbers for sure + if type_value == "data": + if self.master.data_display == "hex": + # Length must be a multiple of 2, and we need to + # assert only hex chars + # Strip the 0x prefix if it exists + if value.lower().startswith("0x"): + value = value[2:] + # Strip spaces, as some programs include them + value = value.replace(" ","") + # Ensure all chars are hex + if [x for x in value if x.lower() not in "0123456789abcdef"]: + # Got non-hex values + if not event == None: + # print("Non-hex character in data!\a") + self.bell() + if not mb.askyesno("Invalid Hex Data","Invalid character in passed hex data.\n\nWould you like to keep editing?",parent=self.parent): + self.destroy() + return + # Ensure we have an even number of chars + if len(value) % 2: + if not event == None: + self.bell() + if not mb.askyesno("Invalid Hex Data","Hex data must contain an even number of chars.\n\nWould you like to keep editing?",parent=self.parent): + self.destroy() + # print("Hex needs an even number of chars!\a") + return + # At this point, we can split our hex into groups of 8 chars and separate with + # a space for easy readability + value = "<{}>".format(" ".join((value[0+i:8+i] for i in range(0, len(value), 8))).upper()) + else: + # Base64 data - we need to make sure all values are within base64 spec, and that we're padded to 4 chars with = + # first we strip the = signs, then verify the data, then, if we have anything, we pad to 4 chars + value = value.rstrip("=") + if [x for x in value if x.lower() not in "0123456789abcdefghijklmnopqrstuvwxyz+/="]: + # Got non-hex values + if not event == None: + # print("Non-hex character in data!\a") + self.bell() + if not mb.askyesno("Invalid Base64 Data","Invalid base64 data passed.\n\nWould you like to keep editing?",parent=self.parent): + self.destroy() + return + if len(value) > 0 and len(value) % 4: + # we have leftover chars - pad to 4 with = + value += "=" * (4-len(value)%4) + # As a last resort, we'll convert it to base64 to verify it's good + try: + test = value + if sys.version_info >= (3,0): + test = test.encode("utf-8") + base64.b64decode(test) + except Exception as e: + # Not the correct format :( + if not event == None: + self.bell() + if not mb.askyesno("Invalid Base64 Data","Invalid base64 data passed.\n\n{}\n\nWould you like to keep editing?".format(str(e)),parent=self.parent): + self.destroy() + return + elif type_value == "date": + # We can take a few options for dates here. + # + # Now/Today + # Mar 11, 2019 12:29:00 PM + # YYYY-MM-DD HH:MM:SS Z + # + if value.lower() in ["now","today"]: + # Get the current date as a datetime object + value = datetime.datetime.now().strftime("%b %d, %Y %I:%M:%S %p") + else: + # Try to parse with strptime + try: + value = datetime.datetime.strptime(value,"%b %d, %Y %I:%M:%S %p").strftime("%b %d, %Y %I:%M:%S %p") + except: + # Try the other method + try: + value = datetime.datetime.strptime(value,"%Y-%m-%d %H:%M:%S %z").strftime("%b %d, %Y %I:%M:%S %p") + except: + # Not the correct format :( + if not event == None: + self.bell() + if not mb.askyesno("Invalid Date","Couldn't convert the passed string to a date.\n\nValid formats include:\nNow/Today\nMar 11, 2019 12:29:00 PM\nYYYY-MM-DD HH:MM:SS Z\n\nWould you like to keep editing?",parent=self.parent): + self.destroy() + return + elif type_value == "number": + # We need to check if we're using hex or decimal + # then verify the chars involved + if value.lower().startswith("0x"): + try: + value = int(value,16) + except: + # Something went wrong + if not event == None: + self.bell() + if not mb.askyesno("Invalid Hex Data","Couldn't convert the passed hex string to an integer.\n\nWould you like to keep editing?",parent=self.parent): + self.destroy() + # print("Could not convert hex!\a") + return + else: + # Not hex, let's try casting as an int first, + # then as a float second - strip any commas + value = value.replace(",","") + try: + value = int(value) + except: + try: + value = float(value) + except: + # Failure! + if not event == None: + # print("Not a number!\a") + self.bell() + if not mb.askyesno("Invalid Number Data","Couldn't convert to an integer or float.\n\nWould you like to keep editing?",parent=self.parent): + self.destroy() + return + # At this point, we should have the decimal value + value = str(value) + # Add to undo stack + self.master.add_undo({"cell":self.cell,"value":original}) + # Replace our value (may be slightly modified) + values[index-1] = value + # Set the values + self.parent.item(self.cell, values=values) + self.destroy() + +class PlistWindow(tk.Toplevel): + def __init__(self, controller, root, **kw): + tk.Toplevel.__init__(self, root, **kw) + os.chdir(os.path.dirname(os.path.realpath(__file__))) + # Create the window + self.root = root + self.controller = controller + self.undo_stack = [] + self.redo_stack = [] + self.drag_undo = None + self.clicked_drag = False + self.data_display = "hex" # hex or base64 + self.menu_code = u"\u21D5" + #self.drag_code = u"\u2630" + self.drag_code = u"\u2261" + + # self = tk.Toplevel(self.root) + self.minsize(width=640,height=480) + self.protocol("WM_DELETE_WINDOW", self.close_window) + w = 640 + h = 480 + # Let's also center the window + x = self.winfo_screenwidth() // 2 - w // 2 + y = self.winfo_screenheight() // 2 - h // 2 + self.geometry("{}x{}+{}+{}".format(w,h, x, y)) + # Set the title to "Untitled.plist" + self.title("Untitled.plist") + + # Set the close window binding + if str(sys.platform) == "darwin": + self.bind("", self.close_window) + else: + self.bind("", self.close_window) + + # Set up the options + self.current_plist = None # None = new + self.edited = False + self.dragging = False + self.drag_start = None + self.type_menu = tk.Menu(self, tearoff=0) + self.type_menu.add_command(label="Dictionary", command=lambda:self.change_type(self.menu_code + " Dictionary")) + self.type_menu.add_command(label="Array", command=lambda:self.change_type(self.menu_code + " Array")) + self.type_menu.add_separator() + self.type_menu.add_command(label="Boolean", command=lambda:self.change_type(self.menu_code + " Boolean")) + self.type_menu.add_command(label="Data", command=lambda:self.change_type(self.menu_code + " Data")) + self.type_menu.add_command(label="Date", command=lambda:self.change_type(self.menu_code + " Date")) + self.type_menu.add_command(label="Number", command=lambda:self.change_type(self.menu_code + " Number")) + self.type_menu.add_command(label="String", command=lambda:self.change_type(self.menu_code + " String")) + + # Set up the boolean selection menu + self.bool_menu = tk.Menu(self, tearoff=0) + self.bool_menu.add_command(label="True", command=lambda:self.set_bool("True")) + self.bool_menu.add_command(label="False", command=lambda:self.set_bool("False")) + + # Create the treeview + self._tree = ttk.Treeview(self, columns=("Type","Value","Drag")) + self._tree.heading("#0", text="Key") + self._tree.heading("#1", text="Type") + self._tree.heading("#2", text="Value") + self._tree.column("Type",width=100,stretch=False) + self._tree.column("Drag",minwidth=40,width=40,stretch=False,anchor="center") + + # Create the scrollbar + vsb = ttk.Scrollbar(self, orient='vertical', command=self._tree.yview) + self._tree.configure(yscrollcommand=vsb.set) + + # Bind right click + if str(sys.platform) == "darwin": + self._tree.bind("", self.popup) # ButtonRelease-2 on mac + else: + self._tree.bind("", self.popup) + + # Set bindings + self._tree.bind("", self.on_double_click) + self._tree.bind("<1>", self.on_single_click) + self._tree.bind('<>', self.tree_click_event) + self._tree.bind('<>', self.pre_alternate) + self._tree.bind('<>', self.alternate_colors) + self._tree.bind("", self.move_selection) + self._tree.bind("",self.confirm_drag) + self._tree.bind("",self.clicked) + self._tree.bind("=", self.new_row) + self._tree.bind("+", self.new_row) + self._tree.bind("-", self.remove_row) + self.bind("", self.got_focus) + + # Setup menu bar (hopefully per-window) - only happens on non-mac systems + if not str(sys.platform) == "darwin": + key="Control" + sign = "Ctr+" + main_menu = tk.Menu(self) + file_menu = tk.Menu(self, tearoff=0) + main_menu.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="New ({}N)".format(sign), command=self.controller.new_plist) + file_menu.add_command(label="Open ({}O)".format(sign), command=self.controller.open_plist) + file_menu.add_command(label="Save ({}S)".format(sign), command=self.controller.save_plist) + file_menu.add_command(label="Save As ({}Shift+S)".format(sign), command=self.controller.save_plist_as) + file_menu.add_command(label="Duplicate ({}D)".format(sign), command=self.controller.duplicate_plist) + file_menu.add_separator() + file_menu.add_command(label="Convert Window ({}T)".format(sign), command=self.controller.show_convert) + file_menu.add_command(label="Strip Comments ({}M)".format(sign), command=self.strip_comments) + file_menu.add_separator() + file_menu.add_command(label="View Data As Hex", command=lambda:self.change_data_display("hex")) + file_menu.add_command(label="View Data As Base64", command=lambda:self.change_data_display("base64")) + if not str(sys.platform) == "darwin": + file_menu.add_separator() + file_menu.add_command(label="Quit ({}Q)".format(sign), command=self.controller.quit) + self.config(menu=main_menu) + + # Get the right click menu options + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.dirname(os.path.realpath(__file__)))) + self.menu_data = {} + if os.path.exists("menu.plist"): + try: + with open("menu.plist","rb") as f: + self.menu_data = plist.load(f) + except: + pass + os.chdir(cwd) + + # Sort dictionary keys? + self.sort_dict = True + + # Add the treeview + vsb.pack(side="right",fill="y") + self._tree.pack(side="bottom", fill="both", expand=True) + self.entry_popup = None + + def get_check_type(self, cell=None, string=None): + if not cell == None: + t = self.get_padded_values(cell,1)[0] + elif not string == None: + t = string + else: + return None + if t.startswith(self.menu_code): + t = t.replace(self.menu_code+" ","") + t = t.replace(self.menu_code,"") + return t + + def clicked(self, event = None): + # Reset every click + self.clicked_drag = False + if not event: + return + column = self._tree.identify_column(event.x) + rowid = self._tree.identify_row(event.y) + if rowid and column == "#3": + # Mouse down in the drag column + self.clicked_drag = True + + def change_data_display(self, new_display = "hex"): + # This will change how data is displayed - we do this by converting all our existing + # data values to bytes, then reconverting and displaying appropriately + if new_display == self.data_display: + # Nothing to do here + return + nodes = self.iter_nodes(False) + removedlist = [] + for node in nodes: + values = self.get_padded_values(node,3) + t = self.get_check_type(node).lower() + value = values[1] + if t == "data": + # We need to adjust how it is displayed, load the bytes first + if new_display == "hex": + # Convert to hex + if sys.version_info < (3,0): + value = binascii.hexlify(base64.b64decode(value)) + else: + value = binascii.hexlify(base64.b64decode(value.encode("utf-8"))).decode("utf-8") + # format the hex + value = "<{}>".format(" ".join((value[0+i:8+i] for i in range(0, len(value), 8))).upper()) + else: + # Assume base64 + if sys.version_info < (3, 0): + value = base64.b64encode(binascii.unhexlify(value.replace("<","").replace(">","").replace(" ",""))) + else: + value = base64.b64encode(binascii.unhexlify(value.replace("<","").replace(">","").replace(" ","").encode("utf-8"))).decode("utf-8") + values[1] = value + self._tree.item(node,values=values) + self.data_display = new_display + + def add_undo(self, action): + if not isinstance(action,list): + action = [action] + self.undo_stack.append(action) + self.redo_stack = [] # clear the redo stack + + def reundo(self, event=None, undo = True): + # Let's come up with a more centralized way to do this + # We'll break down the potential actions into a few types: + # + # add, remove, edit + # + # edited: {"type":"edit","cell":cell_edited,"text":text_value,"values":values_list} + # added: {"type":"add","cell":cell_added} + # removed: {"type":"remove","cell":cell_removed,"from":cell_removed_from,"index":index_in_parent_children} + # moved: {"type":"move","cell":cell_moved,"from":old_parent,"to":new_parent,"index":index_in_old_parent} + # + # All actions are lists of individual changes, in the order they happened. + # If a cell was changed from a Dict to a String, we would have a removed entry + # for each child under that cell, then an edit entry for the cell itself. + # + if undo: + u = self.undo_stack + r = self.redo_stack + else: + r = self.undo_stack + u = self.redo_stack + if not len(u): + self.bell() + # Nothing to undo/redo + return + task_list = u.pop(-1) + r_task_list = [] + # Iterate in reverse to undo the last thing first + for task in task_list[::-1]: + cell = task["cell"] + ttype = task["type"].lower() + if ttype == "edit": + # We changed something in the cell, build a snapshot of the current + r_task_list.append({ + "type":"edit", + "cell":cell, + "text":self._tree.item(cell,"text"), + "values":self._tree.item(cell,"values") + }) + # Now we undo our edit + self._tree.item(cell,text=task["text"],values=task["values"]) + elif ttype == "add": + # We added new things - let's create a removal list + r_task_list.append({ + "type":"remove", + "cell":cell, + "from":self._tree.parent(cell), + "index":self._tree.index(cell) + }) + # Now we actually remove it + self._tree.detach(cell) + elif ttype == "remove": + # We removed this cell, let's attach it to its old parent + r_task_list.append({ + "type":"add", + "cell":cell, + }) + # Now we actually add it + self._tree.move(cell,task["from"],task.get("index","end")) + elif ttype == "move": + # We moved a cell - let's save the old info + r_task_list.append({ + "type":"move", + "cell":cell, + "from":self._tree.parent(cell), + "to":task["from"], + "index":self._tree.index(cell) + }) + # Let's actually move it now + self._tree.move(cell,task["from"],task.get("index","end")) + # Let's check if we have an r_task_list - and add it + if len(r_task_list): + r.append(r_task_list) + # Ensure we're edited + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + self.update_all_children() + self.alternate_colors() + + def got_focus(self, event=None): + # Lift us to the top of the stack order + # only when the window specifically gained focus + if event and event.widget == self: + self.lift() + + def move_selection(self, event): + # Verify we had clicked in the drag column + if not self.clicked_drag: + # Nope, ignore + return + if self.drag_start == None: + # Let's set the drag start + self.drag_start = (event.x, event.y) + return + # Find how far we've drug so far + if not self.dragging: + x, y = self.drag_start + drag_distance = math.sqrt((event.x - x)**2 + (event.y - y)**2) + if drag_distance < 30: + # Not drug enough + return + move_to = self._tree.index(self._tree.identify_row(event.y)) + tv_item = self._tree.identify('item', event.x, event.y) + self._tree.item(tv_item,open=True) + if not self.get_check_type(tv_item).lower() in ["dictionary","array"]: + # Allow adding as child + if not tv_item == "": + tv_item = self._tree.parent(tv_item) + self._tree.item(tv_item,open=True) + # Let's get the bounding box for the target, and if we're in the lower half, + # we'll add as a child, uper half will add as a sibling + else: + rowid = self._tree.identify_row(event.y) + column = self._tree.identify_column(event.x) + x,y,width,height = self._tree.bbox(rowid, column) + if event.y >= y+height/2 and event.y < y+height: + # Just above should add as a sibling + tv_item = self._tree.parent(tv_item) + self._tree.item(tv_item,open=True) + else: + # Just below should add it at item 0 + move_to = 0 + target = self._tree.focus() + if self._tree.index(target) == move_to and tv_item == target: + # Already the same + return + # Save a reference to the item + if not self.drag_undo: + self.drag_undo = {"from":self._tree.parent(target),"index":self._tree.index(target),"name":self._tree.item(target,"text")} + try: + self._tree.move(target, tv_item, move_to) + except: + pass + else: + self._tree.item(target,open=False) + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + self.dragging = True + + def confirm_drag(self, event): + if not self.dragging: + return + self.dragging = False + self.drag_start = None + target = self._tree.focus() + self._tree.item(target,open=True) + node = self._tree.parent(target) + # Finalize the drag undo + undo_tasks = [] + # Add the move command + undo_tasks.append({ + "type":"move", + "cell":target, + "from":self.drag_undo["from"], + "to":node, + "index":self.drag_undo["index"] + }) + # Create a unique name + t = self.get_check_type(node).lower() + verify = t in ["dictionary",""] + if verify: + names = [self._tree.item(x,"text") for x in self._tree.get_children(node) if not x == target] + name = self._tree.item(target,"text") + num = 0 + while True: + temp_name = name if num == 0 else name+" "+str(num) + if temp_name in names: + num += 1 + continue + # Should be good here + name = temp_name + break + self._tree.item(target,text=name) + # Update children first, then check for name change + self.update_all_children() + if self._tree.item(target,"text") != self.drag_undo["name"]: + # Name changed - we need an edit command + undo_tasks.append({ + "type":"edit", + "cell":target, + "text":self.drag_undo["name"], + "values":self._tree.item(target,"values") + }) + # Post the undo, and clear the global + self.add_undo(undo_tasks) + self.drag_undo = None + self.alternate_colors() + + def strip_comments(self, event=None, prefix = "#"): + # Strips out any values attached to keys beginning with "#" + nodes = self.iter_nodes(False) + removedlist = [] + for node in nodes: + name = self._tree.item(node,"text") + if str(name).startswith(prefix): + # Found one, remove it + removedlist.append({ + "type":"remove", + "cell":node, + "from":self._tree.parent(node), + "index":self._tree.index(node) + }) + self._tree.detach(node) + if not len(removedlist): + # Nothing removed + return + # We removed some, flush the changes, update the view, + # post the undo, and make sure we're edited + self.add_undo(removedlist) + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + self.update_all_children() + self.alternate_colors() + + ### ### + # Save/Load Plist Functions # + ### ### + + def check_save(self): + if not self.edited: + return True # No changes, all good + # Post a dialog asking if we want to save the current plist + answer = mb.askyesnocancel("Unsaved Changes", "Save changes to current document?", parent=self) + if answer == True: + return self.save_plist() + return answer + + def save_plist(self, event=None): + # Pass the current plist to the save_plist_as function + return self.save_plist_as(event, self.current_plist) + + def save_plist_as(self, event=None, path=None): + if path == None: + # Get the file dialog + path = fd.asksaveasfilename(parent=self, title = "Please select a file name for saving:",filetypes=[("Plist files", "*.plist")]) + if not len(path): + # User cancelled - no changes + return None + if not path.lower().endswith(".plist"): + path+=".plist" + # Should have the save path + plist_data = self.nodes_to_values() + # Create a temp folder and save there first + temp = tempfile.mkdtemp() + temp_file = os.path.join(temp, os.path.basename(path)) + try: + with open(temp_file,"wb") as f: + plist.dump(plist_data,f) + except Exception as e: + try: + shutil.rmtree(temp,ignore_errors=True) + except: + pass + # Had an issue, throw up a display box + self.bell() + mb.showerror("An Error Occurred While Saving", str(e), parent=self) + return None + try: + # Copy the temp over + shutil.copy(temp_file,path) + except Exception as e: + try: + shutil.rmtree(temp,ignore_errors=True) + except: + pass + # Had an issue, throw up a display box + self.bell() + mb.showerror("An Error Occurred While Saving", str(e), parent=self) + return None + try: + shutil.rmtree(temp,ignore_errors=True) + except: + pass + # Retain the new path if the save worked correctly + self.current_plist = path + # Set the window title to the path + self.title(path) + # No changes - so we'll reset that + self.edited = False + return True + + def open_plist(self, path, plist_data): + # Opened it correctly - let's load it, and set our values + self._tree.delete(*self._tree.get_children()) + self.add_node(plist_data) + self.current_plist = path + if path == None: + self.title("Untitled.plist - Edited") + self.edited = True + else: + self.title(path) + self.edited = False + self.undo_stack = [] + self.redo_stack = [] + self.alternate_colors() + + def close_window(self, event=None): + # Check if we need to save first, then quit if we didn't cancel + if self.check_save() == None: + # User cancelled or we failed to save, bail + return None + # See if we're the only window left, and close the session after + windows = self.stackorder(self.root) + if len(windows) == 1 and windows[0] == self: + # Last and closing + self.root.quit() + else: + self.destroy() + return True + + def paste_selection(self, value): + node = self._tree.focus() + # Verify the type - or get the parent + t = self.get_check_type(node).lower() + if not node == "" and not t in ["dictionary","array"]: + node = self._tree.parent(node) + t = self.get_check_type(node).lower() + verify = t in ["dictionary",""] + dict_list = list(value.items()) if not self.sort_dict else sorted(list(value.items())) + for (key,val) in dict_list: + if verify: + # create a unique name + names = [self._tree.item(x,"text") for x in self._tree.get_children(node)] + name = str(key) + num = 0 + while True: + temp_name = name if num == 0 else name+" "+str(num) + if temp_name in names: + num += 1 + continue + # Should be good here + name = temp_name + break + key = name + last = self.add_node(val, node, key) + self._tree.item(last,open=True) + self.add_undo({"type":"add","cell":last}) + self._tree.focus(last) + self._tree.selection_set(last) + self._tree.update() + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + self.update_all_children() + self.alternate_colors() + + def stackorder(self, root): + """return a list of root and toplevel windows in stacking order (topmost is last)""" + c = root.children + s = root.tk.eval('wm stackorder {}'.format(root)) + L = [x.lstrip('.') for x in s.split()] + return [(c[x] if x else root) for x in L] + + ### ### + # Converstion to/from Dict and Treeview Functions # + ### ### + + def add_node(self, value, parentNode="", key=None): + if key is None: + i = "" + else: + if isinstance(value,(list,tuple)): + children = "1 child" if len(value) == 1 else "{} children".format(len(value)) + values = (self.get_type(value),children,self.drag_code) + elif isinstance(value,dict): + children = "1 key/value pair" if len(value) == 1 else "{} key/value pairs".format(len(value)) + values = (self.get_type(value),children,self.drag_code) + else: + values = (self.get_type(value),value,self.drag_code) + i = self._tree.insert(parentNode, "end", text=key, values=values) + + if isinstance(value, dict): + self._tree.item(i, open=True) + dict_list = list(value.items()) if not self.sort_dict else sorted(list(value.items())) + for (key,val) in dict_list: + self.add_node(val, i, key) + elif isinstance(value, (list,tuple)): + self._tree.item(i, open=True) + for (key,val) in enumerate(value): + self.add_node(val, i, key) + elif self.is_data(value): + self._tree.item(i, values=(self.get_type(value),self.get_data(value),self.drag_code,)) + elif isinstance(value, datetime.datetime): + self._tree.item(i, values=(self.get_type(value),value.strftime("%b %d, %Y %I:%M:%S %p"),self.drag_code,)) + else: + self._tree.item(i, values=(self.get_type(value),value,self.drag_code,)) + return i + + def nodes_to_values(self,node="",parent={}): + if node == "" or node == None: + # top level + parent = {} + for child in self._tree.get_children(node): + parent = self.nodes_to_values(child,parent) + return parent + # Not top - process + name = self._tree.item(node,"text") + values = self.get_padded_values(node, 3) + value = values[1] + check_type = self.get_check_type(node).lower() + # Iterate value types + if check_type == "dictionary": + value = {} + elif check_type == "array": + value = [] + elif check_type == "boolean": + value = True if values[1].lower() == "true" else False + elif check_type == "number": + try: + value = int(value) + except: + try: + value = float(value) + except: + value = 0 # default to 0 if we have to have something + elif check_type == "data": + if self.data_display == "hex": + # Convert the hex + if sys.version_info < (3, 0): + value = plistlib.Data(binascii.unhexlify(value.replace("<","").replace(">","").replace(" ",""))) + else: + value = binascii.unhexlify(value.replace("<","").replace(">","").replace(" ","").encode("utf-8")) + else: + # Assume base64 + if sys.version_info < (3,0): + value = plistlib.Data(base64.b64decode(value)) + else: + value = base64.b64decode(value.encode("utf-8")) + elif check_type == "date": + value = datetime.datetime.strptime(value,"%b %d, %Y %I:%M:%S %p") + # At this point, we should have the name and value + for child in self._tree.get_children(node): + value = self.nodes_to_values(child,value) + if isinstance(parent,list): + parent.append(value) + elif isinstance(parent,dict): + parent[name] = value + return parent + + def get_type(self, value): + if isinstance(value, dict): + return self.menu_code + " Dictionary" + elif isinstance(value, list): + return self.menu_code + " Array" + elif isinstance(value, datetime.datetime): + return self.menu_code + " Date" + elif self.is_data(value): + return self.menu_code + " Data" + elif isinstance(value, bool): + return self.menu_code + " Boolean" + elif isinstance(value, (int,float)): + return self.menu_code + " Number" + elif isinstance(value, str): + return self.menu_code + " String" + else: + return self.menu_code + type(value) + + def is_data(self, value): + if (sys.version_info >= (3, 0) and isinstance(value, bytes)) or (sys.version_info < (3,0) and isinstance(value, plistlib.Data)): + return True + return False + + def get_data(self, value): + if sys.version_info < (3,0) and isinstance(value, plistlib.Data): + value = value.data + if not len(value): + return "<>" if self.data_display == "hex" else "" + if self.data_display == "hex": + h = binascii.hexlify(value) + if sys.version_info >= (3,0): + h = h.decode("utf-8") + return "<{}>".format(" ".join((h[0+i:8+i] for i in range(0, len(h), 8))).upper()) + else: + h = base64.b64encode(value) + if sys.version_info >= (3,0): + h = h.decode("utf-8") + return h + + ### ### + # Node Update Functions # + ### ### + + def new_row(self,target=None,force_sibling=False): + if target == None or isinstance(target, tk.Event): + target = self._tree.focus() + values = self.get_padded_values(target, 1) + new_cell = None + if not self.get_check_type(target).lower() in ["dictionary","array"] or not self._tree.item(target,"open") or force_sibling: + target = self._tree.parent(target) + # create a unique name + names = [self._tree.item(x,"text")for x in self._tree.get_children(target)] + name = "New String" + num = 0 + while True: + temp_name = name if num == 0 else name+" "+str(num) + if temp_name in names: + num += 1 + continue + # Should be good here + name = temp_name + break + new_cell = self._tree.insert(target, "end", text=name, values=(self.menu_code + " String","",self.drag_code,)) + # Verify that array names are updated to show the proper indexes + if self.get_check_type(target).lower() == "array": + self.update_array_counts(target) + # Select and scroll to the target + self._tree.focus(new_cell) + self._tree.selection_set(new_cell) + self._tree.see(new_cell) + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + self.add_undo({"type":"add","cell":new_cell}) + if target == "": + # Top level, nothing to do here but edit the new row + self.alternate_colors() + return + # Update the child counts + self.update_children(target) + # Ensure the target is opened + self._tree.item(target,open=True) + # Flush our alternating lines + self.alternate_colors() + + def remove_row(self,target=None): + if target == None or isinstance(target, tk.Event): + target = self._tree.focus() + if target == "": + # Can't remove top level + return + parent = self._tree.parent(target) + self.add_undo({ + "type":"remove", + "cell":target, + "from":parent, + "index":self._tree.index(target) + }) + self._tree.detach(target) + # self._tree.delete(target) # Removes completely + # Might include an undo function for removals, at least - tbd + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + # Check if the parent was an array/dict, and update counts + if parent == "": + return + if self.get_check_type(parent).lower() == "array": + self.update_array_counts(parent) + self.update_children(parent) + self.alternate_colors() + + ### ### + # Treeview Data Helper Methods # + ### ### + + def get_padded_values(self, item, pad_to = 2): + values = list(self._tree.item(item,"values")) + values = [] if values == "" else values + values += [''] * (pad_to - len(values)) + return values + + def update_all_children(self): + # Iterate the whole list, and ensure all arrays and dicts have their children updated + # properly + nodes = self.iter_nodes(False) + for node in nodes: + check_type = self.get_check_type(node).lower() + if check_type == "dictionary": + self.update_children(node) + elif check_type == "array": + self.update_children(node) + self.update_array_counts(node) + + def update_children(self, target): + # Update the key/value pairs or children count + child_count = self._tree.get_children(target) + values = self.get_padded_values(target, 3) + if self.get_check_type(target).lower() == "dictionary": + children = "1 key/value pair" if len(child_count) == 1 else "{} key/value pairs".format(len(child_count)) + elif self.get_check_type(target).lower() == "array": + children = "1 child" if len(child_count) == 1 else "{} children".format(len(child_count)) + # Set the resulting values + values[1] = children + self._tree.item(target,values=values) + + def update_array_counts(self, target): + for x,child in enumerate(self._tree.get_children(target)): + # Only updating the "text" field + self._tree.item(child,text=x) + + def change_type(self, value): + # Need to walk the values and pad + values = self.get_padded_values(self._tree.focus(), 3) + # Verify we actually changed type + if values[0] == value: + # No change, bail + return + original = [x for x in values] + # Replace our value + values[0] = value + # Remove children if needed + changes = [] + for i in self._tree.get_children(self._tree.focus()): + changes.append({ + "type":"remove", + "cell":i, + "from":self._tree.parent(i), + "index":self._tree.index(i) + }) + self._tree.detach(i) + cell = self._tree.focus() + changes.append({ + "type":"edit", + "cell":cell, + "text":self._tree.item(cell,"text"), + "values":self._tree.item(cell,"values") + }) + # Add to the undo stack + self.add_undo(changes) + # Set the value if need be + value = self.get_check_type(None,value).lower() + if value.lower() == "number": + values[1] = 0 + elif value.lower() == "boolean": + values[1] = "True" + elif value.lower() == "array": + self._tree.item(self._tree.focus(),open=True) + values[1] = "0 children" + elif value.lower() == "dictionary": + self._tree.item(self._tree.focus(),open=True) + values[1] = "0 key/value pairs" + elif value.lower() == "date": + values[1] = datetime.datetime.now().strftime("%b %d, %Y %I:%M:%S %p") + elif value.lower() == "data": + values[1] = "<>" if self.data_display == "hex" else "" + else: + values[1] = "" + # Set the values + self._tree.item(self._tree.focus(), values=values) + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + + ### ### + # Click Functions # + ### ### + + def set_bool(self, value): + # Need to walk the values and pad + values = self.get_padded_values(self._tree.focus(), 3) + cell = self._tree.focus() + self.add_undo({ + "type":"edit", + "cell":cell, + "text":self._tree.item(cell,"text"), + "value":[x for x in values] + }) + values[1] = value + # Set the values + self._tree.item(self._tree.focus(), values=values) + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + + def split(self, a, escape = '\\', separator = '/'): + result = [] + token = '' + state = 0 + for t in a: + if state == 0: + if t == escape: + state = 1 + elif t == separator: + result.append(token) + token = '' + else: + token += t + elif state == 1: + token += t + state = 0 + result.append(token) + return result + + def get_cell_path(self, cell = None): + # Returns the path to the given cell + # Will clear out array indexes - as those should be ignored + if cell == None: + return None + current_cell = cell + path = [] + while True: + if current_cell == "": + # Reached the top + break + cell = self._tree.parent(current_cell) + if not self.get_check_type(cell).lower() == "array": + # Our name isn't just a number add the key + path.append(self._tree.item(current_cell,"text").replace("/","\/")) + else: + path.append("*") + current_cell = cell + return "/".join(path[::-1]) + + def merge_menu_preset(self, val = None): + if val == None: + return + # We need to walk the path of the item to ensure all + # items exist - each should be a dict, unless specified + # by a "*", which denotes a list. + cell,path,itypes,value = val + paths = self.split(str(path)) + types = itypes.split("/") + if not len(paths) == len(types): + self.bell() + mb.showerror("Incorrect Patch Format", "Patch is incomplete.", parent=self) + return + if not len(paths) or paths[0] == "*": + # No path, or it's an array + self.bell() + mb.showerror("Incorrect Patch Format", "Patch starts with an array - must be a dictionary.", parent=self) + return + # Iterate both the paths and types lists - checking for each value, + # and ensuring the type + created = None + current_cell = "" + for p,t in izip(paths,types): + found = False + needed_type = {"d":"Dictionary","a":"Array"}.get(t.lower(),"Dictionary") + for x in self._tree.get_children(current_cell): + cell_name = self._tree.item(x,"text") + if cell_name == p: + current_type = self.get_check_type(x) + if not current_type.lower() == needed_type.lower(): + # Raise an error - type mismatch + self.bell() + mb.showerror("Incorrect Type", "{} is {}, should be {}.".format(cell_name,current_type,needed_type), parent=self) + return + found = True + current_cell = x + break + if not found: + # Need to add it + current_cell = self._tree.insert(current_cell,"end",text=p,values=(self.menu_code+" "+needed_type,"",self.drag_code,),open=True) + if created == None: + # Only get the top level item created + created = current_cell + + # At this point - we should be able to add the final piece + # let's first make sure it doesn't already exist - if it does, we + # will overwrite it + '''current_type = self.get_check_type(current_cell).lower() + if current_type == "dictionary": + # Scan through and make sure we have all the keys needed + for x in self._tree.get_children(current_cell): + name = self._tree.item(x,"text") + if name in value: + # Need to change this one + if len(self._tree.get_children(x)): + # Add some remove commands''' + last_cell = self.add_node(value,current_cell,"") + if created == None: + created = last_cell + self.add_undo({ + "type":"add", + "cell":created + }) + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + self.update_all_children() + self.alternate_colors() + + def popup(self, event): + # Select the item there if possible + cell = self._tree.identify('item', event.x, event.y) + if cell: + self._tree.selection_set(cell) + self._tree.focus(cell) + # Build right click menu + popup_menu = tk.Menu(self, tearoff=0) + # self.popup_menu.add_cascade(label="New", menu=new_menu) + popup_menu.add_command(label="Expand All", command=self.expand_all) + popup_menu.add_command(label="Collapse All", command=self.collapse_all) + popup_menu.add_separator() + # Determine if we are adding a child or a sibling + if cell == "": + # Top level + popup_menu.add_command(label="New top level entry (+)".format(self._tree.item(cell,"text")), command=lambda:self.new_row(cell)) + else: + if self.get_check_type(cell).lower() in ["array","dictionary"] and (self._tree.item(cell,"open") or not len(self._tree.get_children(cell))): + popup_menu.add_command(label="New child under '{}' (+)".format(self._tree.item(cell,"text")), command=lambda:self.new_row(cell)) + popup_menu.add_command(label="New sibling of '{}'".format(self._tree.item(cell,"text")), command=lambda:self.new_row(cell,True)) + popup_menu.add_command(label="Remove '{}' and any children (-)".format(self._tree.item(cell,"text")), command=lambda:self.remove_row(cell)) + else: + popup_menu.add_command(label="New sibling of '{}' (+)".format(self._tree.item(cell,"text")), command=lambda:self.new_row(cell)) + popup_menu.add_command(label="Remove '{}' (-)".format(self._tree.item(cell,"text")), command=lambda:self.remove_row(cell)) + + # Walk through the menu data if it exists + cell_path = self.get_cell_path(cell) + open_core = self.menu_data.get("OpenCore",{}) + clover = self.menu_data.get("Clover",{}) + oc_valid = [x for x in list(open_core) if x.startswith(cell_path)] + cl_valid = [x for x in list(clover) if x.startswith(cell_path)] + if len(oc_valid) or len(cl_valid): + popup_menu.add_separator() + if len(oc_valid): + oc_menu = tk.Menu(popup_menu, tearoff=0) + for item in sorted(oc_valid): + item_menu = tk.Menu(oc_menu, tearoff=0) + for x in open_core[item]: + name = x["name"] + value = x["value"] + types = x["types"] + passed = (cell,item,types,value) + item_menu.add_command(label=name, command=lambda item=passed: self.merge_menu_preset(item)) + oc_menu.add_cascade(label=item,menu=item_menu) + popup_menu.add_cascade(label="OpenCore",menu=oc_menu) + if len(cl_valid): + clover_menu = tk.Menu(popup_menu, tearoff=0) + for item in sorted(cl_valid): + item_menu = tk.Menu(clover_menu, tearoff=0) + for x in clover[item]: + name = x["name"] + value = x["value"] + types = x["types"] + passed = (cell,item,types,value) + item_menu.add_command(label=name, command=lambda item=passed: self.merge_menu_preset(item)) + clover_menu.add_cascade(label=item,menu=item_menu) + popup_menu.add_cascade(label="Clover",menu=clover_menu) + + try: + popup_menu.tk_popup(event.x_root, event.y_root, 0) + except: + pass + finally: + popup_menu.grab_release() + + def expand_all(self): + # Get all nodes + nodes = self.iter_nodes(False) + for node in nodes: + self._tree.item(node,open=True) + self.alternate_colors() + + def collapse_all(self): + # Get all nodes + nodes = self.iter_nodes(False) + for node in nodes: + self._tree.item(node,open=False) + self.alternate_colors() + + def tree_click_event(self, event): + # close previous popups + self.destroy_popups() + + def on_single_click(self, event): + # close previous popups + self.destroy_popups() + + def on_double_click(self, event): + # close previous popups + self.destroy_popups() + # what row and column was clicked on + rowid = self._tree.identify_row(event.y) + column = self._tree.identify_column(event.x) + if rowid == "": + # Nothing double clicked, bail + return + # clicked row parent id + parent = self._tree.parent(rowid) + # get column position info + x,y,width,height = self._tree.bbox(rowid, column) + # get the actual item name we're editing + tv_item = self._tree.identify('item', event.x, event.y) + # y-axis offset + pady = height // 2 + # Get the actual text + index = int(column.replace("#","")) + try: + # t = self._tree.item(rowid,"values")[0] + t = self.get_check_type(rowid) + except: + t = "" + try: + pt = self._tree.item(self._tree.parent(tv_item),"values")[0] + except: + pt = "" + if index == 1: + # Type change - let's show our menu + try: + self.type_menu.tk_popup(event.x_root, event.y_root, 0) + finally: + self.type_menu.grab_release() + return 'break' + if index == 2: + if t.lower() in ["dictionary","array"]: + # Can't edit the "value" directly - should only show the number of children + return 'break' + elif t.lower() == "boolean": + # Bool change + try: + self.bool_menu.tk_popup(event.x_root, event.y_root, 0) + finally: + self.bool_menu.grab_release() + return 'break' + if index == 0: + if pt.lower() == "array": + # No names here, bail + return 'break' + # The name of the item, can be changed at any time + text = self._tree.item(rowid, 'text') + else: + try: + text = self._tree.item(rowid, 'values')[index-1] + except: + text = "" + if index ==2 and t.lower() == "data": + # Special formatting of hex values + text = text.replace("<","").replace(">","") + cell = self._tree.item(self._tree.focus()) + # place Entry popup properly + self.entry_popup = EntryPopup(self._tree, text, tv_item, column) + self.entry_popup.place( x=x, y=y+pady, anchor="w", width=width) + if not self.edited: + self.edited = True + self.title(self.title()+" - Edited") + return 'break' + + ### ### + # Maintenance Functions # + ### ### + + def destroy_popups(self): + # auto-confirm changes + if not self.entry_popup: + return + try: + self.entry_popup.confirm(None) + except: + pass + try: + self.entry_popup.destroy() + except: + pass + self.entry_popup = None + + def iter_nodes(self, visible = True, current_item = None): + items = [] + if current_item == None or isinstance(current_item, tk.Event): + current_item = "" + for child in self._tree.get_children(current_item): + items.append(child) + if not visible or self._tree.item(child,"open"): + items.extend(self.iter_nodes(visible, child)) + return items + + def pre_alternate(self, event): + # Only called before an item opens - we need to open it manually to ensure + # colors alternate correctly + cell = self._tree.focus() + if not self._tree.item(cell,"open"): + self._tree.item(cell,open=True) + # Call the actual alternate_colors function + self.alternate_colors(event) + + def alternate_colors(self, event = None): + # Let's walk the children of our treeview + visible = self.iter_nodes(True,event) + for x,item in enumerate(visible): + tags = self._tree.item(item,"tags") + if not isinstance(tags,list): + tags = [] + # Remove odd or even + try: + tags.remove("odd") + except: + pass + try: + tags.remove("even") + except: + pass + tags.append("odd" if x % 2 else "even") + self._tree.item(item, tags=tags) + self._tree.tag_configure('odd', background='#E8E8E8') + self._tree.tag_configure('even', background='#DFDFDF') \ No newline at end of file diff --git a/Scripts/run.py b/Scripts/run.py new file mode 100644 index 0000000..53c7408 --- /dev/null +++ b/Scripts/run.py @@ -0,0 +1,156 @@ +import sys +import subprocess +import threading +import shlex +try: + from Queue import Queue, Empty +except: + from queue import Queue, Empty + +ON_POSIX = 'posix' in sys.builtin_module_names + +class Run: + + def __init__(self): + return + + def _read_output(self, pipe, q): + try: + for line in iter(lambda: pipe.read(1), b''): + q.put(line) + except ValueError: + pass + pipe.close() + + def _stream_output(self, comm, shell = False): + output = error = "" + p = ot = et = None + try: + if shell and type(comm) is list: + comm = " ".join(shlex.quote(x) for x in comm) + if not shell and type(comm) is str: + comm = shlex.split(comm) + p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=True, close_fds=ON_POSIX) + # Setup the stdout thread/queue + q = Queue() + t = threading.Thread(target=self._read_output, args=(p.stdout, q)) + t.daemon = True # thread dies with the program + # Setup the stderr thread/queue + qe = Queue() + te = threading.Thread(target=self._read_output, args=(p.stderr, qe)) + te.daemon = True # thread dies with the program + # Start both threads + t.start() + te.start() + + while True: + c = z = "" + try: + c = q.get_nowait() + except Empty: + pass + else: + sys.stdout.write(c) + output += c + sys.stdout.flush() + try: + z = qe.get_nowait() + except Empty: + pass + else: + sys.stderr.write(z) + error += z + sys.stderr.flush() + p.poll() + if c==z=="" and p.returncode != None: + break + + o, e = p.communicate() + ot.exit() + et.exit() + return (output+o, error+e, p.returncode) + except: + if ot or et: + try: ot.exit() + except: pass + try: et.exit() + except: pass + if p: + return (output, error, p.returncode) + return ("", "Command not found!", 1) + + def _decode(self, value, encoding="utf-8", errors="ignore"): + # Helper method to only decode if bytes type + if sys.version_info >= (3,0) and isinstance(value, bytes): + return value.decode(encoding,errors) + return value + + def _run_command(self, comm, shell = False): + c = None + try: + if shell and type(comm) is list: + comm = " ".join(shlex.quote(x) for x in comm) + if not shell and type(comm) is str: + comm = shlex.split(comm) + p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + c = p.communicate() + except: + if c == None: + return ("", "Command not found!", 1) + return (self._decode(c[0]), self._decode(c[1]), p.returncode) + + def run(self, command_list, leave_on_fail = False): + # Command list should be an array of dicts + if type(command_list) is dict: + # We only have one command + command_list = [command_list] + output_list = [] + for comm in command_list: + args = comm.get("args", []) + shell = comm.get("shell", False) + stream = comm.get("stream", False) + sudo = comm.get("sudo", False) + stdout = comm.get("stdout", False) + stderr = comm.get("stderr", False) + mess = comm.get("message", None) + show = comm.get("show", False) + + if not mess == None: + print(mess) + + if not len(args): + # nothing to process + continue + if sudo: + # Check if we have sudo + out = self._run_command(["which", "sudo"]) + if "sudo" in out[0]: + # Can sudo + if type(args) is list: + args.insert(0, out[0].replace("\n", "")) # add to start of list + elif type(args) is str: + args = out[0].replace("\n", "") + " " + args # add to start of string + + if show: + print(" ".join(args)) + + if stream: + # Stream it! + out = self._stream_output(args, shell) + else: + # Just run and gather output + out = self._run_command(args, shell) + if stdout and len(out[0]): + print(out[0]) + if stderr and len(out[1]): + print(out[1]) + # Append output + output_list.append(out) + # Check for errors + if leave_on_fail and out[2] != 0: + # Got an error - leave + break + if len(output_list) == 1: + # We only ran one command - just return that output + return output_list[0] + return output_list diff --git a/Scripts/utils.py b/Scripts/utils.py new file mode 100644 index 0000000..a8327dc --- /dev/null +++ b/Scripts/utils.py @@ -0,0 +1,272 @@ +import sys, os, time, re, json, datetime, ctypes, subprocess + +if os.name == "nt": + # Windows + import msvcrt +else: + # Not Windows \o/ + import select + +class Utils: + + def __init__(self, name = "Python Script"): + self.name = name + # Init our colors before we need to print anything + cwd = os.getcwd() + os.chdir(os.path.dirname(os.path.realpath(__file__))) + if os.path.exists("colors.json"): + self.colors_dict = json.load(open("colors.json")) + else: + self.colors_dict = {} + os.chdir(cwd) + + def check_admin(self): + # Returns whether or not we're admin + try: + is_admin = os.getuid() == 0 + except AttributeError: + is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 + return is_admin + + def elevate(self, file): + # Runs the passed file as admin + if self.check_admin(): + return + if os.name == "nt": + ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, file, None, 1) + else: + try: + p = subprocess.Popen(["which", "sudo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + c = p.communicate()[0].decode("utf-8", "ignore").replace("\n", "") + os.execv(c, [ sys.executable, 'python'] + sys.argv) + except: + exit(1) + + def compare_versions(self, vers1, vers2, **kwargs): + # Helper method to compare ##.## strings + # + # vers1 < vers2 = True + # vers1 = vers2 = None + # vers1 > vers2 = False + + # Sanitize the pads + pad = str(kwargs.get("pad", "")) + sep = str(kwargs.get("separator", ".")) + + ignore_case = kwargs.get("ignore_case", True) + + # Cast as strings + vers1 = str(vers1) + vers2 = str(vers2) + + if ignore_case: + vers1 = vers1.lower() + vers2 = vers2.lower() + + # Split and pad lists + v1_parts, v2_parts = self.pad_length(vers1.split(sep), vers2.split(sep)) + + # Iterate and compare + for i in range(len(v1_parts)): + # Remove non-numeric + v1 = ''.join(c.lower() for c in v1_parts[i] if c.isalnum()) + v2 = ''.join(c.lower() for c in v2_parts[i] if c.isalnum()) + # Equalize the lengths + v1, v2 = self.pad_length(v1, v2) + # Compare + if str(v1) < str(v2): + return True + elif str(v1) > str(v2): + return False + # Never differed - return None, must be equal + return None + + def pad_length(self, var1, var2, pad = "0"): + # Pads the vars on the left side to make them equal length + pad = "0" if len(str(pad)) < 1 else str(pad)[0] + if not type(var1) == type(var2): + # Type mismatch! Just return what we got + return (var1, var2) + if len(var1) < len(var2): + if type(var1) is list: + var1.extend([str(pad) for x in range(len(var2) - len(var1))]) + else: + var1 = "{}{}".format((pad*(len(var2)-len(var1))), var1) + elif len(var2) < len(var1): + if type(var2) is list: + var2.extend([str(pad) for x in range(len(var1) - len(var2))]) + else: + var2 = "{}{}".format((pad*(len(var1)-len(var2))), var2) + return (var1, var2) + + def check_path(self, path): + # Loop until we either get a working path - or no changes + count = 0 + while count < 100: + count += 1 + if not len(path): + # We uh.. stripped out everything - bail + return None + if os.path.exists(path): + # Exists! + return os.path.abspath(path) + # Check quotes first + if (path[0] == '"' and path[-1] == '"') or (path[0] == "'" and path[-1] == "'"): + path = path[1:-1] + continue + # Check for tilde + if path[0] == "~": + test_path = os.path.expanduser(path) + if test_path != path: + # We got a change + path = test_path + continue + # If we have no spaces to trim - bail + if not (path[0] == " " or path[0] == " ") and not(path[-1] == " " or path[-1] == " "): + return None + # Here we try stripping spaces/tabs + test_path = path + t_count = 0 + while t_count < 100: + t_count += 1 + t_path = test_path + while len(t_path): + if os.path.exists(t_path): + return os.path.abspath(t_path) + if t_path[-1] == " " or t_path[-1] == " ": + t_path = t_path[:-1] + continue + break + if test_path[0] == " " or test_path[0] == " ": + test_path = test_path[1:] + continue + break + # Escapes! + test_path = "\\".join([x.replace("\\", "") for x in path.split("\\\\")]) + if test_path != path and not (path[0] == " " or path[0] == " "): + path = test_path + continue + if path[0] == " " or path[0] == " ": + path = path[1:] + return None + + def grab(self, prompt, **kwargs): + # Takes a prompt, a default, and a timeout and shows it with that timeout + # returning the result + timeout = kwargs.get("timeout", 0) + default = kwargs.get("default", None) + # If we don't have a timeout - then skip the timed sections + if timeout <= 0: + if sys.version_info >= (3, 0): + return input(prompt) + else: + return str(raw_input(prompt)) + # Write our prompt + sys.stdout.write(prompt) + sys.stdout.flush() + if os.name == "nt": + start_time = time.time() + i = '' + while True: + if msvcrt.kbhit(): + c = msvcrt.getche() + if ord(c) == 13: # enter_key + break + elif ord(c) >= 32: #space_char + i += c + if len(i) == 0 and (time.time() - start_time) > timeout: + break + else: + i, o, e = select.select( [sys.stdin], [], [], timeout ) + if i: + i = sys.stdin.readline().strip() + print('') # needed to move to next line + if len(i) > 0: + return i + else: + return default + + def cls(self): + os.system('cls' if os.name=='nt' else 'clear') + + def cprint(self, message, **kwargs): + strip_colors = kwargs.get("strip_colors", False) + if os.name == "nt": + strip_colors = True + reset = u"\u001b[0m" + # Requires sys import + for c in self.colors: + if strip_colors: + message = message.replace(c["find"], "") + else: + message = message.replace(c["find"], c["replace"]) + if strip_colors: + return message + sys.stdout.write(message) + print(reset) + + # Needs work to resize the string if color chars exist + '''# Header drawing method + def head(self, text = None, width = 55): + if text == None: + text = self.name + self.cls() + print(" {}".format("#"*width)) + len_text = self.cprint(text, strip_colors=True) + mid_len = int(round(width/2-len(len_text)/2)-2) + middle = " #{}{}{}#".format(" "*mid_len, len_text, " "*((width - mid_len - len(len_text))-2)) + if len(middle) > width+1: + # Get the difference + di = len(middle) - width + # Add the padding for the ...# + di += 3 + # Trim the string + middle = middle[:-di] + newlen = len(middle) + middle += "...#" + find_list = [ c["find"] for c in self.colors ] + + # Translate colored string to len + middle = middle.replace(len_text, text + self.rt_color) # always reset just in case + self.cprint(middle) + print("#"*width)''' + + # Header drawing method + def head(self, text = None, width = 55): + if text == None: + text = self.name + self.cls() + print(" {}".format("#"*width)) + mid_len = int(round(width/2-len(text)/2)-2) + middle = " #{}{}{}#".format(" "*mid_len, text, " "*((width - mid_len - len(text))-2)) + if len(middle) > width+1: + # Get the difference + di = len(middle) - width + # Add the padding for the ...# + di += 3 + # Trim the string + middle = middle[:-di] + "...#" + print(middle) + print("#"*width) + + def resize(self, width, height): + print('\033[8;{};{}t'.format(height, width)) + + def custom_quit(self): + self.head() + print("by CorpNewt\n") + print("Thanks for testing it out, for bugs/comments/complaints") + print("send me a message on Reddit, or check out my GitHub:\n") + print("www.reddit.com/u/corpnewt") + print("www.github.com/corpnewt\n") + # Get the time and wish them a good morning, afternoon, evening, and night + hr = datetime.datetime.now().time().hour + if hr > 3 and hr < 12: + print("Have a nice morning!\n\n") + elif hr >= 12 and hr < 17: + print("Have a nice afternoon!\n\n") + elif hr >= 17 and hr < 21: + print("Have a nice evening!\n\n") + else: + print("Have a nice night!\n\n") + exit(0)