├── .gitattributes ├── .gitignore ├── LICENSE ├── OCConfigCompare.bat ├── OCConfigCompare.command ├── OCConfigCompare.py ├── README.md └── Scripts ├── __init__.py ├── downloader.py ├── plist.py └── utils.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ensure all .bat scripts use CRLF line endings 2 | # This can prevent a number of odd batch issues 3 | *.bat text eol=crlf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Sample plist 7 | Sample.plist 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CorpNewt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OCConfigCompare.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Get our local path before delayed expansion - allows ! in path 3 | set "thisDir=%~dp0" 4 | 5 | setlocal enableDelayedExpansion 6 | REM Setup initial vars 7 | set "script_name=" 8 | set /a tried=0 9 | set "toask=yes" 10 | set "pause_on_error=yes" 11 | set "py2v=" 12 | set "py2path=" 13 | set "py3v=" 14 | set "py3path=" 15 | set "pypath=" 16 | set "targetpy=3" 17 | 18 | REM use_py3: 19 | REM TRUE = Use if found, use py2 otherwise 20 | REM FALSE = Use py2 21 | REM FORCE = Use py3 22 | set "use_py3=TRUE" 23 | 24 | REM We'll parse if the first argument passed is 25 | REM --install-python and if so, we'll just install 26 | set "just_installing=FALSE" 27 | 28 | REM Get the system32 (or equivalent) path 29 | call :getsyspath "syspath" 30 | 31 | REM Make sure the syspath exists 32 | if "!syspath!" == "" ( 33 | if exist "%SYSTEMROOT%\system32\cmd.exe" ( 34 | if exist "%SYSTEMROOT%\system32\reg.exe" ( 35 | if exist "%SYSTEMROOT%\system32\where.exe" ( 36 | REM Fall back on the default path if it exists 37 | set "ComSpec=%SYSTEMROOT%\system32\cmd.exe" 38 | set "syspath=%SYSTEMROOT%\system32\" 39 | ) 40 | ) 41 | ) 42 | if "!syspath!" == "" ( 43 | cls 44 | echo ### ### 45 | echo # Warning # 46 | echo ### ### 47 | echo. 48 | echo Could not locate cmd.exe, reg.exe, or where.exe 49 | echo. 50 | echo Please ensure your ComSpec environment variable is properly configured and 51 | echo points directly to cmd.exe, then try again. 52 | echo. 53 | echo Current CompSpec Value: "%ComSpec%" 54 | echo. 55 | echo Press [enter] to quit. 56 | pause > nul 57 | exit /b 1 58 | ) 59 | ) 60 | 61 | if "%~1" == "--install-python" ( 62 | set "just_installing=TRUE" 63 | goto installpy 64 | ) 65 | 66 | goto checkscript 67 | 68 | :checkscript 69 | REM Check for our script first 70 | set "looking_for=!script_name!" 71 | if "!script_name!" == "" ( 72 | set "looking_for=%~n0.py or %~n0.command" 73 | set "script_name=%~n0.py" 74 | if not exist "!thisDir!\!script_name!" ( 75 | set "script_name=%~n0.command" 76 | ) 77 | ) 78 | if not exist "!thisDir!\!script_name!" ( 79 | echo Could not find !looking_for!. 80 | echo Please make sure to run this script from the same directory 81 | echo as !looking_for!. 82 | echo. 83 | echo Press [enter] to quit. 84 | pause > nul 85 | exit /b 1 86 | ) 87 | goto checkpy 88 | 89 | :checkpy 90 | call :updatepath 91 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 92 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) 93 | for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) 94 | REM Walk our returns to see if we need to install 95 | if /i "!use_py3!" == "FALSE" ( 96 | set "targetpy=2" 97 | set "pypath=!py2path!" 98 | ) else if /i "!use_py3!" == "FORCE" ( 99 | set "pypath=!py3path!" 100 | ) else if /i "!use_py3!" == "TRUE" ( 101 | set "pypath=!py3path!" 102 | if "!pypath!" == "" set "pypath=!py2path!" 103 | ) 104 | if not "!pypath!" == "" ( 105 | goto runscript 106 | ) 107 | if !tried! lss 1 ( 108 | if /i "!toask!"=="yes" ( 109 | REM Better ask permission first 110 | goto askinstall 111 | ) else ( 112 | goto installpy 113 | ) 114 | ) else ( 115 | cls 116 | echo ### ### 117 | echo # Warning # 118 | echo ### ### 119 | echo. 120 | REM Couldn't install for whatever reason - give the error message 121 | echo Python is not installed or not found in your PATH var. 122 | echo Please install it from https://www.python.org/downloads/windows/ 123 | echo. 124 | echo Make sure you check the box labeled: 125 | echo. 126 | echo "Add Python X.X to PATH" 127 | echo. 128 | echo Where X.X is the py version you're installing. 129 | echo. 130 | echo Press [enter] to quit. 131 | pause > nul 132 | exit /b 1 133 | ) 134 | goto runscript 135 | 136 | :checkpylauncher 137 | REM Attempt to check the latest python 2 and 3 versions via the py launcher 138 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 139 | for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) 140 | goto :EOF 141 | 142 | :checkpyversion 143 | set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( 144 | REM Ensure we have a version number 145 | call :isnumber "%%a" 146 | if not "!errorlevel!" == "0" goto :EOF 147 | set "version=%%a" 148 | ) 149 | if not defined version goto :EOF 150 | if "!version:~0,1!" == "2" ( 151 | REM Python 2 152 | call :comparepyversion "!version!" "!%~2!" 153 | if "!errorlevel!" == "1" ( 154 | set "%~2=!version!" 155 | set "%~3=%~1" 156 | ) 157 | ) else ( 158 | REM Python 3 159 | call :comparepyversion "!version!" "!%~4!" 160 | if "!errorlevel!" == "1" ( 161 | set "%~4=!version!" 162 | set "%~5=%~1" 163 | ) 164 | ) 165 | goto :EOF 166 | 167 | :isnumber 168 | set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i 169 | if defined var (exit /b 1) 170 | exit /b 0 171 | 172 | :comparepyversion 173 | REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 174 | for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( 175 | set a1=%%a 176 | set a2=%%b 177 | set a3=%%c 178 | ) 179 | for /f "tokens=1,2,3 delims=." %%a in ("%~2") do ( 180 | set b1=%%a 181 | set b2=%%b 182 | set b3=%%c 183 | ) 184 | if not defined a1 set a1=0 185 | if not defined a2 set a2=0 186 | if not defined a3 set a3=0 187 | if not defined b1 set b1=0 188 | if not defined b2 set b2=0 189 | if not defined b3 set b3=0 190 | if %a1% gtr %b1% exit /b 1 191 | if %a1% lss %b1% exit /b 2 192 | if %a2% gtr %b2% exit /b 1 193 | if %a2% lss %b2% exit /b 2 194 | if %a3% gtr %b3% exit /b 1 195 | if %a3% lss %b3% exit /b 2 196 | exit /b 0 197 | 198 | :askinstall 199 | cls 200 | echo ### ### 201 | echo # Python Not Found # 202 | echo ### ### 203 | echo. 204 | echo Python !targetpy! was not found on the system or in the PATH var. 205 | echo. 206 | set /p "menu=Would you like to install it now? [y/n]: " 207 | if /i "!menu!"=="y" ( 208 | REM We got the OK - install it 209 | goto installpy 210 | ) else if "!menu!"=="n" ( 211 | REM No OK here... 212 | set /a tried=!tried!+1 213 | goto checkpy 214 | ) 215 | REM Incorrect answer - go back 216 | goto askinstall 217 | 218 | :installpy 219 | REM This will attempt to download and install python 220 | REM First we get the html for the python downloads page for Windows 221 | set /a tried=!tried!+1 222 | cls 223 | echo ### ### 224 | echo # Installing Python # 225 | echo ### ### 226 | echo. 227 | echo Gathering info from https://www.python.org/downloads/windows/... 228 | powershell -command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" 229 | REM Extract it if it's gzip compressed 230 | powershell -command "$infile='%TEMP%\pyurl.txt';$outfile='%TEMP%\pyurl.temp';try{$input=New-Object System.IO.FileStream $infile,([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read);$output=New-Object System.IO.FileStream $outfile,([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::None);$gzipStream=New-Object System.IO.Compression.GzipStream $input,([IO.Compression.CompressionMode]::Decompress);$buffer=New-Object byte[](1024);while($true){$read=$gzipstream.Read($buffer,0,1024);if($read -le 0){break};$output.Write($buffer,0,$read)};$gzipStream.Close();$output.Close();$input.Close();Move-Item -Path $outfile -Destination $infile -Force}catch{}" 231 | if not exist "%TEMP%\pyurl.txt" ( 232 | if /i "!just_installing!" == "TRUE" ( 233 | echo Failed to get info 234 | exit /b 1 235 | ) else ( 236 | goto checkpy 237 | ) 238 | ) 239 | echo Parsing for latest... 240 | pushd "%TEMP%" 241 | :: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) 242 | for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) 243 | popd 244 | if "!release!" == "" ( 245 | if /i "!just_installing!" == "TRUE" ( 246 | echo Failed to get python version 247 | exit /b 1 248 | ) else ( 249 | goto checkpy 250 | ) 251 | ) 252 | echo Found Python !release! - Downloading... 253 | REM Let's delete our txt file now - we no longer need it 254 | del "%TEMP%\pyurl.txt" 255 | REM At this point - we should have the version number. 256 | REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" 257 | set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" 258 | set "pytype=exe" 259 | if "!targetpy!" == "2" ( 260 | set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" 261 | set "pytype=msi" 262 | ) 263 | REM Now we download it with our slick powershell command 264 | powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" 265 | REM If it doesn't exist - we bail 266 | if not exist "%TEMP%\pyinstall.!pytype!" ( 267 | if /i "!just_installing!" == "TRUE" ( 268 | echo Failed to download installer 269 | exit /b 1 270 | ) else ( 271 | goto checkpy 272 | ) 273 | ) 274 | REM It should exist at this point - let's run it to install silently 275 | echo Installing... 276 | pushd "%TEMP%" 277 | if /i "!pytype!" == "exe" ( 278 | echo pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 279 | pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 280 | ) else ( 281 | set "foldername=!release:.=!" 282 | echo msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 283 | msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" 284 | ) 285 | popd 286 | echo Installer finished with %ERRORLEVEL% status. 287 | REM Now we should be able to delete the installer and check for py again 288 | del "%TEMP%\pyinstall.!pytype!" 289 | REM If it worked, then we should have python in our PATH 290 | REM this does not get updated right away though - let's try 291 | REM manually updating the local PATH var 292 | call :updatepath 293 | if /i "!just_installing!" == "TRUE" ( 294 | echo. 295 | echo Done. 296 | ) else ( 297 | goto checkpy 298 | ) 299 | exit /b 300 | 301 | :runscript 302 | REM Python found 303 | cls 304 | set "args=%*" 305 | set "args=!args:"=!" 306 | if "!args!"=="" ( 307 | "!pypath!" "!thisDir!!script_name!" 308 | ) else ( 309 | "!pypath!" "!thisDir!!script_name!" %* 310 | ) 311 | if /i "!pause_on_error!" == "yes" ( 312 | if not "%ERRORLEVEL%" == "0" ( 313 | echo. 314 | echo Script exited with error code: %ERRORLEVEL% 315 | echo. 316 | echo Press [enter] to exit... 317 | pause > nul 318 | ) 319 | ) 320 | goto :EOF 321 | 322 | :undouble 323 | REM Helper function to strip doubles of a single character out of a string recursively 324 | set "string_value=%~2" 325 | :undouble_continue 326 | set "check=!string_value:%~3%~3=%~3!" 327 | if not "!check!" == "!string_value!" ( 328 | set "string_value=!check!" 329 | goto :undouble_continue 330 | ) 331 | set "%~1=!check!" 332 | goto :EOF 333 | 334 | :updatepath 335 | set "spath=" 336 | set "upath=" 337 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKCU\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "upath=%%j" ) 338 | for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "spath=%%j" ) 339 | if not "%spath%" == "" ( 340 | REM We got something in the system path 341 | set "PATH=%spath%" 342 | if not "%upath%" == "" ( 343 | REM We also have something in the user path 344 | set "PATH=%PATH%;%upath%" 345 | ) 346 | ) else if not "%upath%" == "" ( 347 | set "PATH=%upath%" 348 | ) 349 | REM Remove double semicolons from the adjusted PATH 350 | call :undouble "PATH" "%PATH%" ";" 351 | goto :EOF 352 | 353 | :getsyspath 354 | REM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by 355 | REM walking the ComSpec var - will also repair it in memory if need be 356 | REM Strip double semi-colons 357 | call :undouble "temppath" "%ComSpec%" ";" 358 | 359 | REM Dirty hack to leverage the "line feed" approach - there are some odd side 360 | REM effects with this. Do not use this variable name in comments near this 361 | REM line - as it seems to behave erradically. 362 | (set LF=^ 363 | %=this line is empty=% 364 | ) 365 | REM Replace instances of semi-colons with a line feed and wrap 366 | REM in parenthesis to work around some strange batch behavior 367 | set "testpath=%temppath:;=!LF!%" 368 | 369 | REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there 370 | set /a found=0 371 | for /f "tokens=* delims=" %%i in ("!testpath!") do ( 372 | REM Only continue if we haven't found it yet 373 | if not "%%i" == "" ( 374 | if !found! lss 1 ( 375 | set "checkpath=%%i" 376 | REM Remove "cmd.exe" from the end if it exists 377 | if /i "!checkpath:~-7!" == "cmd.exe" ( 378 | set "checkpath=!checkpath:~0,-7!" 379 | ) 380 | REM Pad the end with a backslash if needed 381 | if not "!checkpath:~-1!" == "\" ( 382 | set "checkpath=!checkpath!\" 383 | ) 384 | REM Let's see if cmd, reg, and where exist there - and set it if so 385 | if EXIST "!checkpath!cmd.exe" ( 386 | if EXIST "!checkpath!reg.exe" ( 387 | if EXIST "!checkpath!where.exe" ( 388 | set /a found=1 389 | set "ComSpec=!checkpath!cmd.exe" 390 | set "%~1=!checkpath!" 391 | ) 392 | ) 393 | ) 394 | ) 395 | ) 396 | ) 397 | goto :EOF 398 | -------------------------------------------------------------------------------- /OCConfigCompare.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the curent directory, the script name 4 | # and the script name with "py" substituted for the extension. 5 | args=( "$@" ) 6 | dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" 7 | script="${0##*/}" 8 | target="${script%.*}.py" 9 | 10 | # use_py3: 11 | # TRUE = Use if found, use py2 otherwise 12 | # FALSE = Use py2 13 | # FORCE = Use py3 14 | use_py3="TRUE" 15 | 16 | # We'll parse if the first argument passed is 17 | # --install-python and if so, we'll just install 18 | just_installing="FALSE" 19 | 20 | tempdir="" 21 | 22 | compare_to_version () { 23 | # Compares our OS version to the passed OS version, and 24 | # return a 1 if we match the passed compare type, or a 0 if we don't. 25 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 26 | # $2 = OS version to compare ours to 27 | if [ -z "$1" ] || [ -z "$2" ]; then 28 | # Missing info - bail. 29 | return 30 | fi 31 | local current_os= comp= 32 | current_os="$(sw_vers -productVersion)" 33 | comp="$(vercomp "$current_os" "$2")" 34 | # Check gequal and lequal first 35 | if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then 36 | # Matched 37 | echo "1" 38 | else 39 | # No match 40 | echo "0" 41 | fi 42 | } 43 | 44 | set_use_py3_if () { 45 | # Auto sets the "use_py3" variable based on 46 | # conditions passed 47 | # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) 48 | # $2 = OS version to compare 49 | # $3 = TRUE/FALSE/FORCE in case of match 50 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 51 | # Missing vars - bail with no changes. 52 | return 53 | fi 54 | if [ "$(compare_to_version "$1" "$2")" == "1" ]; then 55 | use_py3="$3" 56 | fi 57 | } 58 | 59 | get_remote_py_version () { 60 | local pyurl= py_html= py_vers= py_num="3" 61 | pyurl="https://www.python.org/downloads/macos/" 62 | py_html="$(curl -L $pyurl --compressed 2>&1)" 63 | if [ -z "$use_py3" ]; then 64 | use_py3="TRUE" 65 | fi 66 | if [ "$use_py3" == "FALSE" ]; then 67 | py_num="2" 68 | fi 69 | py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" 70 | echo "$py_vers" 71 | } 72 | 73 | download_py () { 74 | local vers="$1" url= 75 | clear 76 | echo " ### ###" 77 | echo " # Downloading Python #" 78 | echo "### ###" 79 | echo 80 | if [ -z "$vers" ]; then 81 | echo "Gathering latest version..." 82 | vers="$(get_remote_py_version)" 83 | fi 84 | if [ -z "$vers" ]; then 85 | # Didn't get it still - bail 86 | print_error 87 | fi 88 | echo "Located Version: $vers" 89 | echo 90 | echo "Building download url..." 91 | url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }')" 92 | if [ -z "$url" ]; then 93 | # Couldn't get the URL - bail 94 | print_error 95 | fi 96 | echo " - $url" 97 | echo 98 | echo "Downloading..." 99 | echo 100 | # Create a temp dir and download to it 101 | tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" 102 | curl "$url" -o "$tempdir/python.pkg" 103 | if [ "$?" != "0" ]; then 104 | echo 105 | echo " - Failed to download python installer!" 106 | echo 107 | exit $? 108 | fi 109 | echo 110 | echo "Running python install package..." 111 | echo 112 | sudo installer -pkg "$tempdir/python.pkg" -target / 113 | if [ "$?" != "0" ]; then 114 | echo 115 | echo " - Failed to install python!" 116 | echo 117 | exit $? 118 | fi 119 | # Now we expand the package and look for a shell update script 120 | pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" 121 | if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then 122 | # Run the script 123 | echo 124 | echo "Updating PATH..." 125 | echo 126 | "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" 127 | fi 128 | vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" 129 | if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then 130 | # Certs script exists - let's execute that to make sure our certificates are updated 131 | echo 132 | echo "Updating Certificates..." 133 | echo 134 | "/Applications/$vers_folder/Install Certificates.command" 135 | fi 136 | echo 137 | echo "Cleaning up..." 138 | cleanup 139 | echo 140 | if [ "$just_installing" == "TRUE" ]; then 141 | echo "Done." 142 | else 143 | # Now we check for py again 144 | echo "Rechecking py..." 145 | downloaded="TRUE" 146 | clear 147 | main 148 | fi 149 | } 150 | 151 | cleanup () { 152 | if [ -d "$tempdir" ]; then 153 | rm -Rf "$tempdir" 154 | fi 155 | } 156 | 157 | print_error() { 158 | clear 159 | cleanup 160 | echo " ### ###" 161 | echo " # Python Not Found #" 162 | echo "### ###" 163 | echo 164 | echo "Python is not installed or not found in your PATH var." 165 | echo 166 | if [ "$kernel" == "Darwin" ]; then 167 | echo "Please go to https://www.python.org/downloads/macos/ to" 168 | echo "download and install the latest version, then try again." 169 | else 170 | echo "Please install python through your package manager and" 171 | echo "try again." 172 | fi 173 | echo 174 | exit 1 175 | } 176 | 177 | print_target_missing() { 178 | clear 179 | cleanup 180 | echo " ### ###" 181 | echo " # Target Not Found #" 182 | echo "### ###" 183 | echo 184 | echo "Could not locate $target!" 185 | echo 186 | exit 1 187 | } 188 | 189 | format_version () { 190 | local vers="$1" 191 | echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" 192 | } 193 | 194 | vercomp () { 195 | # Modified from: https://apple.stackexchange.com/a/123408/11374 196 | local ver1="$(format_version "$1")" ver2="$(format_version "$2")" 197 | if [ $ver1 -gt $ver2 ]; then 198 | echo "1" 199 | elif [ $ver1 -lt $ver2 ]; then 200 | echo "2" 201 | else 202 | echo "0" 203 | fi 204 | } 205 | 206 | get_local_python_version() { 207 | # $1 = Python bin name (defaults to python3) 208 | # Echoes the path to the highest version of the passed python bin if any 209 | local py_name="$1" max_version= python= python_version= python_path= 210 | if [ -z "$py_name" ]; then 211 | py_name="python3" 212 | fi 213 | py_list="$(which -a "$py_name" 2>/dev/null)" 214 | # Walk that newline separated list 215 | while read python; do 216 | if [ -z "$python" ]; then 217 | # Got a blank line - skip 218 | continue 219 | fi 220 | if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then 221 | # See if we have a valid developer path 222 | xcode-select -p > /dev/null 2>&1 223 | if [ "$?" != "0" ]; then 224 | # /usr/bin/python3 path - but no valid developer dir 225 | continue 226 | fi 227 | fi 228 | python_version="$(get_python_version $python)" 229 | if [ -z "$python_version" ]; then 230 | # Didn't find a py version - skip 231 | continue 232 | fi 233 | # Got the py version - compare to our max 234 | if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then 235 | # Max not set, or less than the current - update it 236 | max_version="$python_version" 237 | python_path="$python" 238 | fi 239 | done <<< "$py_list" 240 | echo "$python_path" 241 | } 242 | 243 | get_python_version() { 244 | local py_path="$1" py_version= 245 | # Get the python version by piping stderr into stdout (for py2), then grepping the output for 246 | # the word "python", getting the second element, and grepping for an alphanumeric version number 247 | py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" 248 | if [ ! -z "$py_version" ]; then 249 | echo "$py_version" 250 | fi 251 | } 252 | 253 | prompt_and_download() { 254 | if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then 255 | # We already tried to download, or we're not on macOS - just bail 256 | print_error 257 | fi 258 | clear 259 | echo " ### ###" 260 | echo " # Python Not Found #" 261 | echo "### ###" 262 | echo 263 | target_py="Python 3" 264 | printed_py="Python 2 or 3" 265 | if [ "$use_py3" == "FORCE" ]; then 266 | printed_py="Python 3" 267 | elif [ "$use_py3" == "FALSE" ]; then 268 | target_py="Python 2" 269 | printed_py="Python 2" 270 | fi 271 | echo "Could not locate $printed_py!" 272 | echo 273 | echo "This script requires $printed_py to run." 274 | echo 275 | while true; do 276 | read -p "Would you like to install the latest $target_py now? (y/n): " yn 277 | case $yn in 278 | [Yy]* ) download_py;break;; 279 | [Nn]* ) print_error;; 280 | esac 281 | done 282 | } 283 | 284 | main() { 285 | local python= version= 286 | # Verify our target exists 287 | if [ ! -f "$dir/$target" ]; then 288 | # Doesn't exist 289 | print_target_missing 290 | fi 291 | if [ -z "$use_py3" ]; then 292 | use_py3="TRUE" 293 | fi 294 | if [ "$use_py3" != "FALSE" ]; then 295 | # Check for py3 first 296 | python="$(get_local_python_version python3)" 297 | fi 298 | if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then 299 | # We aren't using py3 explicitly, and we don't already have a path 300 | python="$(get_local_python_version python2)" 301 | if [ -z "$python" ]; then 302 | # Try just looking for "python" 303 | python="$(get_local_python_version python)" 304 | fi 305 | fi 306 | if [ -z "$python" ]; then 307 | # Didn't ever find it - prompt 308 | prompt_and_download 309 | return 1 310 | fi 311 | # Found it - start our script and pass all args 312 | "$python" "$dir/$target" "${args[@]}" 313 | } 314 | 315 | # Keep track of whether or not we're on macOS to determine if 316 | # we can download and install python for the user as needed. 317 | kernel="$(uname -s)" 318 | # Check to see if we need to force based on 319 | # macOS version. 10.15 has a dummy python3 version 320 | # that can trip up some py3 detection in other scripts. 321 | # set_use_py3_if "3" "10.15" "FORCE" 322 | downloaded="FALSE" 323 | # Check for the aforementioned /usr/bin/python3 stub if 324 | # our OS version is 10.15 or greater. 325 | check_py3_stub="$(compare_to_version "3" "10.15")" 326 | trap cleanup EXIT 327 | if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then 328 | just_installing="TRUE" 329 | download_py 330 | else 331 | main 332 | fi 333 | -------------------------------------------------------------------------------- /OCConfigCompare.py: -------------------------------------------------------------------------------- 1 | from Scripts import downloader, plist, utils 2 | from collections import deque 3 | import os, plistlib, json, datetime, sys, argparse, copy, datetime, shutil, binascii, re 4 | 5 | try: 6 | long 7 | unicode 8 | except NameError: # Python 3 9 | long = int 10 | unicode = str 11 | 12 | class OCCC: 13 | def __init__(self): 14 | self.d = downloader.Downloader() 15 | self.u = utils.Utils("OC Config Compare") 16 | if 2/3 == 0: 17 | self.dict_types = (dict,plistlib._InternalDict) 18 | else: 19 | self.dict_types = (dict) 20 | self.w = 80 21 | self.h = 24 22 | if os.name == "nt": 23 | self.w = 120 24 | self.h = 30 25 | os.system("color") # Allow ansi commands 26 | self.current_config = None 27 | self.current_plist = None 28 | self.sample_plist = None 29 | self.sample_url = "https://github.com/acidanthera/OpenCorePkg/raw/{}/Docs/Sample.plist" 30 | self.opencorpgk_url = "https://api.github.com/repos/acidanthera/OpenCorePkg/releases" 31 | self.sample_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),os.path.basename(self.sample_url)) 32 | self.settings_file = os.path.join(os.path.dirname(os.path.realpath(__file__)),"Scripts","settings.json") 33 | self.settings = {} 34 | """Smol default settings dict = { 35 | "hide_with_prefix" : ["#","PciRoot","4D1EDE05-","4D1FDA02-","7C436110-","8BE4DF61-"], 36 | "prefix_case_sensitive" : True, 37 | "suppress_warnings" : True, 38 | "update_user" : False, 39 | "update_sample" : False, 40 | "no_timestamp" : False, 41 | "backup_original" : False, 42 | "resize_window" : True, 43 | "compare_values" : False, 44 | "compare_in_arrays" : False # Overrides compare_values 45 | }""" 46 | self.default_hide = ["#","PciRoot","4D1EDE05-","4D1FDA02-","7C436110-","8BE4DF61-"] 47 | if os.path.exists(self.settings_file): 48 | try: 49 | self.settings = json.load(open(self.settings_file)) 50 | except: 51 | pass 52 | self.sample_config = self.sample_path if os.path.exists(self.sample_path) else None 53 | if self.sample_config: 54 | try: 55 | with open(self.sample_config,"rb") as f: 56 | self.sample_plist = plist.load(f) 57 | except: 58 | self.sample_plist = self.sample_config = None 59 | 60 | def get_value(self, value): 61 | if self.is_data(value): 62 | return "0x"+binascii.hexlify(value).decode().upper() 63 | return value 64 | 65 | def is_data(self, value): 66 | return (sys.version_info >= (3,0) and isinstance(value, bytes)) or (sys.version_info < (3,0) and isinstance(value, plistlib.Data)) 67 | 68 | def get_type(self, value): 69 | if isinstance(value, dict): 70 | return "Dictionary" 71 | elif isinstance(value, list): 72 | return "Array" 73 | elif isinstance(value, datetime.datetime): 74 | return "Date" 75 | elif self.is_data(value): 76 | return "Data" 77 | elif isinstance(value, bool): 78 | return "Boolean" 79 | elif isinstance(value, (int,long)): 80 | return "Integer" 81 | elif isinstance(value, float): 82 | return "Real" 83 | elif isinstance(value, (str,unicode)): 84 | return "String" 85 | else: 86 | return str(type(value)) 87 | 88 | def get_timestamp(self,name="config.plist",backup=False): 89 | needs_plist = name.lower().endswith(".plist") 90 | if needs_plist: 91 | name = name[:-6] # Strip the .plist extension 92 | name = "{}-{}{}".format(name,"backup-" if backup else "",datetime.datetime.today().strftime("%Y-%m-%d-%H.%M")) 93 | if needs_plist: 94 | name += ".plist" # Add it to the end again 95 | return name 96 | 97 | def sorted_nicely(self, l, reverse = False): 98 | convert = lambda text: int(text) if text.isdigit() else text 99 | alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key.lower())] 100 | return sorted(l,key=lambda x:alphanum_key(x),reverse=reverse) 101 | 102 | def compare(self,hide=False): 103 | # First make sure we have plist info 104 | c = self.get_plist("user config.plist",self.current_config,hide=hide) 105 | if c is None: 106 | return 107 | self.current_config,self.current_plist = c 108 | # Get the latest release if we don't have one - or use the one we have 109 | if self.sample_config is None: 110 | s = self.get_latest(wait=False) 111 | else: 112 | s = self.get_plist("OC Sample.plist",self.sample_config,hide=hide) 113 | if s is None: 114 | return 115 | self.sample_config,self.sample_plist = s 116 | if not hide: 117 | self.u.head() 118 | print("\nGathering differences...") 119 | p_string = "" 120 | p_string += "\nChecking for values missing from User plist:\n\n" 121 | user_copy = copy.deepcopy(self.current_plist) if self.settings.get("update_user",False) else None 122 | user_missing = self.sorted_nicely(self.compare_value( 123 | self.sample_plist, 124 | self.current_plist, 125 | path=os.path.basename(self.current_config), 126 | to_copy=user_copy!=None, 127 | compare_copy=user_copy, 128 | compare_values=self.settings.get("compare_values",self.settings.get("compare_in_arrays",False)), 129 | compare_in_arrays=self.settings.get("compare_in_arrays",False) 130 | )) 131 | p_string += "\n".join(user_missing) if len(user_missing) else " - Nothing missing from User config!" 132 | p_string += "\n\nChecking for values missing from Sample:\n\n" 133 | sample_copy = copy.deepcopy(self.sample_plist) if self.settings.get("update_sample",False) else None 134 | sample_missing = self.sorted_nicely(self.compare_value( 135 | self.current_plist, 136 | self.sample_plist, 137 | path=os.path.basename(self.sample_config), 138 | to_copy=sample_copy!=None, 139 | compare_copy=sample_copy, 140 | compare_values=False, # Only do this to show changes from defaults in the user plist 141 | compare_in_arrays=False 142 | )) 143 | p_string += "\n".join(sample_missing) if len(sample_missing) else " - Nothing missing from Sample config!" 144 | p_string += "\n" 145 | for l,c,p in ((user_missing,user_copy,self.current_config),(sample_missing,sample_copy,self.sample_config)): 146 | if c!=None and len([x for x in l if not x.lower().endswith(": skipped")]): 147 | path = os.path.dirname(p) 148 | name = os.path.basename(p) 149 | if self.settings.get("backup_original",False): 150 | backup_name = self.get_timestamp(name,backup=True) 151 | p_string += "\nBacking up {} -> {}...".format(name,backup_name) 152 | shutil.copy(p,os.path.join(path,backup_name)) 153 | elif not self.settings.get("no_timestamp",False): 154 | name = self.get_timestamp(name) 155 | p_string += "\nUpdating {} with changes...".format(name) 156 | try: 157 | with open(os.path.join(path,name),"wb") as f: 158 | plist.dump(c,f) 159 | except Exception as e: 160 | p_string += "\nError saving {}: {}".format(name,str(e)) 161 | p_string += "\n" 162 | w = max([len(x) for x in p_string.split("\n")])+1 163 | h = 5 + len(p_string.split("\n")) 164 | if not hide: 165 | if self.settings.get("resize_window",True): 166 | self.u.resize(w if w > self.w else self.w, h if h > self.h else self.h) 167 | self.u.head() 168 | print(p_string) 169 | if not hide: 170 | self.u.grab("Press [enter] to return...") 171 | 172 | def starts_with(self, value, prefixes=None): 173 | if prefixes is None: 174 | prefixes = self.settings.get("hide_with_prefix",self.default_hide) 175 | if prefixes is None: 176 | return False # Nothing passed, and nothing in settings - everything is allowed 177 | case_sensitive = self.settings.get("prefix_case_sensitive",True) 178 | if not case_sensitive: # Normalize case 179 | prefixes = [x.lower() for x in prefixes] if isinstance(prefixes,(list,tuple)) else prefixes.lower() 180 | value = value.lower() 181 | if isinstance(prefixes,list): 182 | prefixes = tuple(prefixes) # Convert to tuple if need be 183 | if not isinstance(prefixes,tuple): 184 | prefixes = (prefixes,) # Wrap up in tuple as needed 185 | return value.startswith(prefixes) 186 | 187 | def get_valid_keys(self, check_dict): 188 | return [x for x in check_dict if not self.starts_with(x,prefixes=None)] 189 | 190 | def compare_value(self, compare_from, compare_to, to_copy=False, compare_copy=None, path="", compare_values=False, compare_in_arrays=False): 191 | # Set up a depth-first iterative loop to avoid max recursion 192 | compare_stack = deque() 193 | compare_stack.append(( 194 | compare_from, 195 | compare_to, 196 | to_copy, 197 | compare_copy, 198 | path, 199 | compare_values, 200 | compare_in_arrays 201 | )) 202 | change_list = deque() 203 | while compare_stack: 204 | compare = compare_stack.popleft() 205 | # Compare 206 | children,changes = self._compare_value(*compare) 207 | if changes: 208 | # We got some changes - add them to our list 209 | change_list.extend(changes) 210 | if children: 211 | # We got child elements - let's add them to the 212 | # stack 213 | compare_stack.extend(children) 214 | return change_list 215 | 216 | def _compare_value(self, compare_from, compare_to, to_copy=False, compare_copy=None, path="", compare_values=False, compare_in_arrays=False): 217 | change_list = deque() 218 | children = deque() 219 | # Compare 2 collections and print anything that's in compare_from that's not in compare_to 220 | if type(compare_from) != type(compare_to): # Should only happen if it's top level differences 221 | change_list.append("{} - Type Difference: {} --> {}".format(path,self.get_type(compare_to),self.get_type(compare_from))) 222 | elif isinstance(compare_from,self.dict_types): 223 | # Let's compare keys 224 | not_keys = self.get_valid_keys([x for x in list(compare_from) if not x in list(compare_to)]) 225 | for x in not_keys: 226 | if to_copy: 227 | compare_copy[x] = compare_from[x] 228 | change_list.append("{} - Missing Key: {}".format(path,x)) 229 | # Let's verify all other values if needed 230 | for x in list(compare_from): 231 | if x in not_keys: 232 | continue # Skip these as they're already not in the _to 233 | if self.starts_with(x): 234 | continue # Skipping this due to prefix 235 | if type(compare_from[x]) != type(compare_to[x]): 236 | if to_copy: 237 | compare_copy[x] = compare_from[x] 238 | change_list.append("{} - Type Difference: {} --> {}".format(path+" -> "+x,self.get_type(compare_to[x]),self.get_type(compare_from[x]))) 239 | continue # Move forward as all underlying values will be different too 240 | if isinstance(compare_from[x],list) or isinstance(compare_from[x],self.dict_types): 241 | children.append(( 242 | compare_from[x], 243 | compare_to[x], 244 | to_copy, 245 | compare_copy[x] if to_copy else None, 246 | path+" -> "+x, 247 | compare_values, 248 | compare_in_arrays 249 | )) 250 | elif compare_values and compare_from[x] != compare_to[x]: 251 | # Checking all values - and our value is different 252 | change_list.append("{} - Value Difference: {} --> {}".format( 253 | path+" -> "+x, 254 | self.get_value(compare_to[x]), 255 | self.get_value(compare_from[x]) 256 | )) 257 | elif isinstance(compare_from,list): 258 | # This will be tougher, but we should only check for dict children and compare keys 259 | if not len(compare_from) or not len(compare_to): 260 | if not self.settings.get("suppress_warnings",True): 261 | change_list.append(path+" -> {}-Array - Empty: Skipped".format("From|To" if not len(compare_from) and not len(compare_to) else "From" if not len(compare_from) else "To")) 262 | elif not compare_in_arrays and not all((isinstance(x,self.dict_types) for x in compare_from)): 263 | if not self.settings.get("suppress_warnings",True): 264 | change_list.append(path+" -> From-Array - Non-Dictionary Children: Skipped") 265 | else: 266 | # Ensure we list if the arrays are different length 267 | min_count = min(len(compare_from),len(compare_to)) 268 | if compare_in_arrays and len(compare_from) != len(compare_to): 269 | change_list.append( 270 | "{} - From|To-Array Lengths Differ: {:,} --> {:,}, Checking Indices 0-{:,}".format( 271 | path, 272 | len(compare_from), 273 | len(compare_to), 274 | min_count-1 275 | ) 276 | ) 277 | # Check if they're all dicts 278 | if compare_in_arrays and not all((isinstance(x,self.dict_types) for x in compare_from)): 279 | # We're checking in arrays and the child elements aren't all dicts 280 | for i in range(min_count): 281 | children.append(( 282 | compare_from[i], 283 | compare_to[i], 284 | to_copy, 285 | compare_copy[i] if to_copy else None, 286 | path+" -> Array[{}]".format(i), 287 | compare_in_arrays, # We use our compare_in_arrays value here as arrays are spammy 288 | compare_in_arrays 289 | )) 290 | else: 291 | # All children of compare_from are dicts - let's ensure consistent keys 292 | valid_keys = [] 293 | for x in compare_from: 294 | valid_keys.extend(self.get_valid_keys(x)) 295 | valid_keys = set(valid_keys) 296 | global_keys = [] 297 | for key in valid_keys: 298 | if all((key in x for x in compare_from)): 299 | global_keys.append(key) 300 | global_keys = set(global_keys) 301 | if not global_keys: 302 | if not self.settings.get("suppress_warnings",True): 303 | change_list.append(path+" -> From-Array - All Child Keys Differ: Skipped") 304 | else: 305 | if global_keys != valid_keys: 306 | if not self.settings.get("suppress_warnings",True): 307 | change_list.append(path+" -> From-Array - Child Keys Differ: Checking Consistent") 308 | # Compare keys, pull consistent placeholders from compare_placeholder 309 | for i,check in enumerate(compare_to): 310 | if i >= len(compare_from): 311 | # Out of range 312 | break 313 | if global_keys != valid_keys: 314 | # Build a key placeholder to check using only consistent keys 315 | compare_placeholder = {} 316 | for key in global_keys: 317 | compare_placeholder[key] = compare_from[i][key] 318 | else: 319 | # Just use the next in line 320 | compare_placeholder = compare_from[i] 321 | children.append(( 322 | compare_placeholder, 323 | compare_to[i], 324 | to_copy, 325 | compare_copy[i] if to_copy else None, 326 | path+" -> Array[{}]".format(i), 327 | compare_in_arrays, # We use our compare_in_arrays value here as arrays are spammy 328 | compare_in_arrays 329 | )) 330 | elif compare_values and compare_from != compare_to: 331 | # Just for checking top level non-collection values 332 | change_list.append("{} - Value Difference: {} --> {}".format( 333 | path, 334 | self.get_value(compare_to), 335 | self.get_value(compare_from) 336 | )) 337 | return (children,change_list) 338 | 339 | def get_latest(self,use_release=True,wait=True,hide=False): 340 | if not hide: 341 | if self.settings.get("resize_window",True): 342 | self.u.resize(self.w,self.h) 343 | self.u.head() 344 | print("") 345 | if use_release: 346 | # Get the tag name 347 | try: 348 | urlsource = json.loads(self.d.get_string(self.opencorpgk_url,False)) 349 | repl = urlsource[0]["tag_name"] 350 | except: 351 | repl = "master" # fall back on the latest commit if failed 352 | else: 353 | repl = "master" 354 | dl_url = self.sample_url.format(repl) 355 | print("Gathering latest Sample.plist from:") 356 | print(dl_url) 357 | print("") 358 | p = None 359 | dl_config = self.d.stream_to_file(dl_url,self.sample_path) 360 | if not dl_config: 361 | print("\nFailed to download!\n") 362 | if wait: 363 | self.u.grab("Press [enter] to return...") 364 | return None 365 | print("Loading...") 366 | try: 367 | with open(dl_config,"rb") as f: 368 | p = plist.load(f) 369 | except Exception as e: 370 | print("\nPlist failed to load: {}\n".format(e)) 371 | if wait: 372 | self.u.grab("Press [enter] to return...") 373 | return None 374 | print("") 375 | if wait: 376 | self.u.grab("Press [enter] to return...") 377 | return (dl_config,p) 378 | 379 | def get_plist(self,plist_name="config.plist",plist_path=None,hide=False): 380 | if not hide and self.settings.get("resize_window",True): 381 | self.u.resize(self.w,self.h) 382 | while True: 383 | if plist_path != None: 384 | m = plist_path 385 | else: 386 | self.u.head() 387 | print("") 388 | print("M. Return to Menu") 389 | print("Q. Quit") 390 | print("") 391 | m = self.u.grab("Please drag and drop the {} file: ".format(plist_name)) 392 | if m.lower() == "m": 393 | return None 394 | elif m.lower() == "q": 395 | self.u.custom_quit() 396 | plist_path = None # Reset 397 | pl = self.u.check_path(m) 398 | if not pl: 399 | self.u.head() 400 | print("") 401 | self.u.grab("That path does not exist!",timeout=5) 402 | continue 403 | try: 404 | with open(pl,"rb") as f: 405 | p = plist.load(f) 406 | except Exception as e: 407 | self.u.head() 408 | print("") 409 | self.u.grab("Plist ({}) failed to load: {}".format(os.path.basename(pl),e),timeout=5) 410 | continue 411 | return (pl,p) # Return the path and plist contents 412 | 413 | def print_hide_keys(self): 414 | hide_keys = self.settings.get("hide_with_prefix",self.default_hide) 415 | if isinstance(hide_keys,(list,tuple)): 416 | return ", ".join(hide_keys) 417 | return hide_keys 418 | 419 | def custom_hide_prefix(self): 420 | self.u.head() 421 | print("") 422 | print("Key Hide Prefixes: {}".format(self.print_hide_keys())) 423 | print("") 424 | pref = self.u.grab("Please enter the custom hide key prefix: ") 425 | return pref if len(pref) else None 426 | 427 | def remove_prefix(self): 428 | prefixes = self.settings.get("hide_with_prefix",self.default_hide) 429 | if prefixes != None and not isinstance(prefixes,(list,tuple)): 430 | prefixes = [prefixes] 431 | while True: 432 | self.u.head() 433 | print("") 434 | print("Key Hide Prefixes:") 435 | print("") 436 | if prefixes == None or not len(prefixes): 437 | print(" - None") 438 | else: 439 | for i,x in enumerate(prefixes,start=1): 440 | print("{}. {}".format(i,x)) 441 | print("") 442 | print("A. Remove All") 443 | print("M. Prefix Menu") 444 | print("Q. Quit") 445 | print("") 446 | pref = self.u.grab("Please enter the number of the prefix to remove: ").lower() 447 | if not len(pref): 448 | continue 449 | if pref == "m": 450 | return None if prefixes == None or not len(prefixes) else prefixes 451 | if pref == "q": 452 | self.u.custom_quit() 453 | if pref == "a": 454 | return None 455 | if prefixes == None: 456 | continue # Nothing to remove and not a menu option 457 | else: # Hope for a number 458 | try: 459 | pref = int(pref)-1 460 | assert 0 <= pref < len(prefixes) 461 | except: 462 | continue 463 | del prefixes[pref] 464 | 465 | def hide_key_prefix(self): 466 | while True: 467 | self.u.head() 468 | print("") 469 | print("Key Hide Prefixes: {}".format(self.print_hide_keys())) 470 | print("Suppress Warnings: {}".format(self.settings.get("suppress_warnings",True))) 471 | print("Compare Values: {}".format( 472 | "True (+ Arrays)" if self.settings.get("compare_in_arrays") else "True" if self.settings.get("compare_values") else "False" 473 | )) 474 | print("") 475 | print("1. Hide Only Keys Starting With #") 476 | print("2. Hide comments (#), PciRoot, and most OC NVRAM samples") 477 | print("3. Add New Custom Prefix") 478 | print("4. Remove Prefix") 479 | print("5. Show All Keys") 480 | print("6. {} Warnings".format("Show" if self.settings.get("suppress_warnings",True) else "Suppress")) 481 | print("7. Toggle Compare Values") 482 | print("") 483 | print("M. Main Menu") 484 | print("Q. Quit") 485 | print("") 486 | menu = self.u.grab("Please select an option: ") 487 | if menu.lower() == "m": 488 | return 489 | elif menu.lower() == "q": 490 | self.u.custom_quit() 491 | elif menu == "1": 492 | self.settings["hide_with_prefix"] = "#" 493 | elif menu == "2": 494 | self.settings["hide_with_prefix"] = ["#","PciRoot","4D1EDE05-","4D1FDA02-","7C436110-","8BE4DF61-"] 495 | elif menu == "3": 496 | new_prefix = self.custom_hide_prefix() 497 | if not new_prefix: 498 | continue # Nothing to add 499 | prefixes = self.settings.get("hide_with_prefix",self.default_hide) 500 | if prefixes == None: 501 | prefixes = new_prefix # None set yet 502 | elif isinstance(prefixes,(list,tuple)): # It's a list or tuple 503 | if new_prefix in prefixes: 504 | continue # Already in the list 505 | prefixes = list(prefixes) 506 | prefixes.append(new_prefix) 507 | else: 508 | if prefixes == new_prefix: 509 | continue # Already set to that 510 | prefixes = [prefixes,new_prefix] # Is a string, probably 511 | self.settings["hide_with_prefix"] = prefixes 512 | elif menu == "4": 513 | self.settings["hide_with_prefix"] = self.remove_prefix() 514 | elif menu == "5": 515 | self.settings["hide_with_prefix"] = None 516 | elif menu == "6": 517 | self.settings["suppress_warnings"] = not self.settings.get("suppress_warnings",True) 518 | elif menu == "7": 519 | if self.settings.get("compare_in_arrays"): # Disable all 520 | self.settings["compare_in_arrays"] = self.settings["compare_values"] = False 521 | elif self.settings.get("compare_values"): # Switch to arrays 522 | self.settings["compare_in_arrays"] = self.settings["compare_values"] = True 523 | else: # Just compare values 524 | self.settings["compare_in_arrays"] = False 525 | self.settings["compare_values"] = True 526 | self.save_settings() 527 | 528 | def save_settings(self): 529 | try: 530 | json.dump(self.settings,open(self.settings_file,"w"),indent=2) 531 | except: 532 | pass 533 | 534 | def main(self): 535 | if self.settings.get("resize_window",True): 536 | self.u.resize(self.w,self.h) 537 | self.u.head() 538 | print("") 539 | print("Current Config: {}".format(self.current_config)) 540 | print("OC Sample Config: {}".format(self.sample_config)) 541 | print("Key Hide Prefixes: {}".format(self.print_hide_keys())) 542 | print("Prefix Case-Sensitive: {}".format(self.settings.get("prefix_case_sensitive",True))) 543 | print("Suppress Warnings: {}".format(self.settings.get("suppress_warnings",True))) 544 | print("Compare Values: {}".format( 545 | "True (+ Arrays)" if self.settings.get("compare_in_arrays") else "True" if self.settings.get("compare_values") else "False" 546 | )) 547 | print("") 548 | print("1. Get Latest Release Sample.plist") 549 | print("2. Get Latest Commit Sample.plist") 550 | print("3. Select Local Sample.plist") 551 | print("4. Select Local User Config.plist") 552 | print("5. Change Key Hide Prefixes/Warnings/Compare Values") 553 | print("6. Toggle Prefix Case-Sensitivity") 554 | print("7. Compare (will use latest Sample.plist if none selected)") 555 | print("") 556 | print("Q. Quit") 557 | print("") 558 | m = self.u.grab("Please select an option: ").lower() 559 | if m == "q": 560 | if self.settings.get("resize_window",True): 561 | self.u.resize(self.w,self.h) 562 | self.u.custom_quit() 563 | elif m in ("1","2"): 564 | p = self.get_latest(use_release=m=="1") 565 | if p is not None: 566 | self.sample_config,self.sample_plist = p 567 | elif m == "3": 568 | p = self.get_plist("OC Sample.plist") 569 | if p is not None: 570 | self.sample_config,self.sample_plist = p 571 | elif m == "4": 572 | p = self.get_plist("user config.plist") 573 | if p is not None: 574 | self.current_config,self.current_plist = p 575 | elif m == "5": 576 | self.hide_key_prefix() 577 | elif m == "6": 578 | self.settings["prefix_case_sensitive"] = not self.settings.get("prefix_case_sensitive",True) 579 | self.save_settings() 580 | elif m == "7": 581 | self.compare() 582 | 583 | def cli(self, user_plist = None, sample_plist = None, use_release = False): 584 | # Let's normalize the plist paths - and use the latest sample.plist if no sample passed 585 | if not user_plist: 586 | print("User plist path is required!") 587 | exit(1) 588 | user_plist = self.u.check_path(user_plist) 589 | if not user_plist: 590 | print("User plist path invalid!") 591 | exit(1) 592 | # Try to load it 593 | try: 594 | with open(user_plist, "rb") as f: 595 | user_plist_data = plist.load(f) 596 | except Exception as e: 597 | print("User plist failed to load! {}".format(e)) 598 | exit(1) 599 | # It loads - save it 600 | self.current_config = user_plist 601 | # Check the sample_plist as needed 602 | if sample_plist: 603 | sample_plist = self.u.check_path(sample_plist) 604 | if not sample_plist: 605 | print("Sample plist path invalid!") 606 | exit(1) 607 | # Try to load it 608 | try: 609 | with open(sample_plist, "rb") as f: 610 | sample_plist_data = plist.load(f) 611 | except Exception as e: 612 | print("Sample plist failed to load! {}".format(e)) 613 | exit(1) 614 | # Loads - we should be good - save it 615 | self.sample_config = sample_plist 616 | else: 617 | # Let's get the latest commit 618 | p = self.get_latest(use_release=use_release,wait=False,hide=True) 619 | if not p: 620 | print("Could not get the latest sample!") 621 | exit(1) 622 | self.sample_config,self.sample_plist = p 623 | self.compare(hide=True) 624 | 625 | if __name__ == '__main__': 626 | parser = argparse.ArgumentParser() 627 | parser.add_argument("-u","--user-plist",help="Path to the local user plist.") 628 | parser.add_argument("-s","--sample-plist",help="Path to the sample plist - will get the latest commit from OC if none passed.") 629 | parser.add_argument("-r","--use-release",help="Get the latest release sample instead of the latest commit if none passed.",action="store_true") 630 | parser.add_argument("-w","--suppress-warnings",help="Yes/no (default: yes), sets if non-essential warnings (empty lists, etc) show when comparing - overrides settings.",nargs="?",const="1") 631 | parser.add_argument("-v","--verbose",help="Print more verbose output - forces '-w yes' and '-n' - overrides settings.",action="store_true") 632 | parser.add_argument("-x","--hide-prefix",help="Prefix to hide when comparing.",action="append") 633 | parser.add_argument("-n","--no-prefix",help="Clears all hide prefixes - overrides '-x' and settings.",action="store_true") 634 | parser.add_argument("-c","--case-sensitive",help="Yes/no (default: yes), sets hide prefix case-sensitivity - overrides settings.",nargs="?",const="1") 635 | parser.add_argument("-m","--compare-values",help="Yes/no/array (default: no), check for value differences as well - overrides settings.",nargs="?",const="1") 636 | parser.add_argument("-d","--dev-help",help="Show the help menu with developer options visible.",action="store_true") 637 | parser.add_argument("-p","--update-user",help=argparse.SUPPRESS,action="store_true") 638 | parser.add_argument("-l","--update-sample",help=argparse.SUPPRESS,action="store_true") 639 | parser.add_argument("-t","--no-timestamp",help=argparse.SUPPRESS,action="store_true") 640 | parser.add_argument("-b","--backup-original",help=argparse.SUPPRESS,action="store_true") 641 | args = parser.parse_args() 642 | 643 | if args.dev_help: # Update the developer options help, and show it 644 | update = { 645 | "update_user":"Pull changes into a timestamped copy (unless overridden by -t or -b) of the user plist.", 646 | "update_sample":"Pull changes into a timestamped copy (unless overridden by -t or -b) of the sample plist.", 647 | "no_timestamp":"Pull changes directly into the user or sample plist without a timestamped copy (requires -p or -l).", 648 | "backup_original":"Backup the user or sample plist with a timestamp before replacing it directly (requires -p or -l, overrides -t)" 649 | } 650 | for action in parser._actions: 651 | if not action.dest in update: 652 | continue 653 | action.help = update[action.dest] 654 | parser.print_help() 655 | exit() 656 | 657 | o = OCCC() 658 | def get_yes_no(val): 659 | val = str(val).lower() 660 | if val in ("y","on","yes","true","1","enable","enabled"): 661 | return True 662 | if val in ("n","off","no","false","0","disable","disabled"): 663 | return False 664 | return None 665 | if args.suppress_warnings: 666 | yn = get_yes_no(args.suppress_warnings) 667 | if yn is not None: 668 | o.settings["suppress_warnings"] = yn 669 | if args.case_sensitive: 670 | yn = get_yes_no(args.case_sensitive) 671 | if yn is not None: 672 | o.settings["prefix_case_sensitive"] = yn 673 | if args.compare_values: 674 | if args.compare_values.lower() in ("a","array","arrays"): 675 | o.settings["compare_values"] = o.settings["compare_in_arrays"] = True 676 | else: 677 | yn = get_yes_no(args.compare_values) 678 | if yn is not None: 679 | o.settings["compare_in_arrays"] = False 680 | o.settings["compare_values"] = yn 681 | if args.update_user: 682 | o.settings["update_user"] = True 683 | if args.update_sample: 684 | o.settings["update_sample"] = True 685 | if args.no_timestamp: 686 | o.settings["no_timestamp"] = True 687 | if args.no_prefix: 688 | o.settings["hide_with_prefix"] = None 689 | if args.hide_prefix: 690 | o.settings["hide_with_prefix"] = [x for x in args.hide_prefix if x] 691 | if args.verbose: 692 | # Force warnings and remove any hidden prefixes 693 | o.settings["suppress_warnings"] = False 694 | o.settings["hide_with_prefix"] = None 695 | if args.backup_original: 696 | o.settings["no_timestamp"] = False 697 | o.settings["backup_original"] = True 698 | if args.user_plist or args.sample_plist: 699 | # We got a required arg - start in cli mode 700 | o.cli(args.user_plist,args.sample_plist,use_release=args.use_release) 701 | exit() 702 | 703 | if 2/3 == 0: 704 | input = raw_input 705 | while True: 706 | try: 707 | o.main() 708 | except Exception as e: 709 | print("\nError: {}\n".format(e)) 710 | input("Press [enter] to continue...") 711 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OCConfigCompare 2 | Python script to compare two plists and list missing keys in either. 3 | 4 | *** 5 | 6 | ``` 7 | usage: OCConfigCompare.py [-h] [-u USER_PLIST] [-s SAMPLE_PLIST] [-r] 8 | [-w [SUPPRESS_WARNINGS]] [-v] [-x HIDE_PREFIX] [-n] 9 | [-c [CASE_SENSITIVE]] [-m [COMPARE_VALUES]] [-d] 10 | 11 | options: 12 | -h, --help show this help message and exit 13 | -u, --user-plist USER_PLIST 14 | Path to the local user plist. 15 | -s, --sample-plist SAMPLE_PLIST 16 | Path to the sample plist - will get the latest commit 17 | from OC if none passed. 18 | -r, --use-release Get the latest release sample instead of the latest 19 | commit if none passed. 20 | -w, --suppress-warnings [SUPPRESS_WARNINGS] 21 | Yes/no (default: yes), sets if non-essential warnings 22 | (empty lists, etc) show when comparing - overrides 23 | settings. 24 | -v, --verbose Print more verbose output - forces '-w yes' and '-n' - 25 | overrides settings. 26 | -x, --hide-prefix HIDE_PREFIX 27 | Prefix to hide when comparing. 28 | -n, --no-prefix Clears all hide prefixes - overrides '-x' and 29 | settings. 30 | -c, --case-sensitive [CASE_SENSITIVE] 31 | Yes/no (default: yes), sets hide prefix case- 32 | sensitivity - overrides settings. 33 | -m, --compare-values [COMPARE_VALUES] 34 | Yes/no/array (default: no), check for value 35 | differences as well - overrides settings. 36 | -d, --dev-help Show the help menu with developer options visible. 37 | ``` 38 | -------------------------------------------------------------------------------- /Scripts/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile 2 | import glob 3 | modules = glob.glob(dirname(__file__)+"/*.py") 4 | __all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] -------------------------------------------------------------------------------- /Scripts/downloader.py: -------------------------------------------------------------------------------- 1 | import sys, os, time, ssl, gzip, multiprocessing 2 | from io import BytesIO 3 | # Python-aware urllib stuff 4 | try: 5 | from urllib.request import urlopen, Request 6 | import queue as q 7 | except ImportError: 8 | # Import urllib2 to catch errors 9 | import urllib2 10 | from urllib2 import urlopen, Request 11 | import Queue as q 12 | 13 | TERMINAL_WIDTH = 120 if os.name=="nt" else 80 14 | 15 | def get_size(size, suffix=None, use_1024=False, round_to=2, strip_zeroes=False): 16 | # size is the number of bytes 17 | # suffix is the target suffix to locate (B, KB, MB, etc) - if found 18 | # use_2014 denotes whether or not we display in MiB vs MB 19 | # round_to is the number of dedimal points to round our result to (0-15) 20 | # strip_zeroes denotes whether we strip out zeroes 21 | 22 | # Failsafe in case our size is unknown 23 | if size == -1: 24 | return "Unknown" 25 | # Get our suffixes based on use_1024 26 | ext = ["B","KiB","MiB","GiB","TiB","PiB"] if use_1024 else ["B","KB","MB","GB","TB","PB"] 27 | div = 1024 if use_1024 else 1000 28 | s = float(size) 29 | s_dict = {} # Initialize our dict 30 | # Iterate the ext list, and divide by 1000 or 1024 each time to setup the dict {ext:val} 31 | for e in ext: 32 | s_dict[e] = s 33 | s /= div 34 | # Get our suffix if provided - will be set to None if not found, or if started as None 35 | suffix = next((x for x in ext if x.lower() == suffix.lower()),None) if suffix else suffix 36 | # Get the largest value that's still over 1 37 | biggest = suffix if suffix else next((x for x in ext[::-1] if s_dict[x] >= 1), "B") 38 | # Determine our rounding approach - first make sure it's an int; default to 2 on error 39 | try:round_to=int(round_to) 40 | except:round_to=2 41 | round_to = 0 if round_to < 0 else 15 if round_to > 15 else round_to # Ensure it's between 0 and 15 42 | bval = round(s_dict[biggest], round_to) 43 | # Split our number based on decimal points 44 | a,b = str(bval).split(".") 45 | # Check if we need to strip or pad zeroes 46 | b = b.rstrip("0") if strip_zeroes else b.ljust(round_to,"0") if round_to > 0 else "" 47 | return "{:,}{} {}".format(int(a),"" if not b else "."+b,biggest) 48 | 49 | def _process_hook(queue, total_size, bytes_so_far=0, update_interval=1.0, max_packets=0): 50 | packets = [] 51 | speed = remaining = "" 52 | last_update = time.time() 53 | while True: 54 | # Write our info first so we have *some* status while 55 | # waiting for packets 56 | if total_size > 0: 57 | percent = float(bytes_so_far) / total_size 58 | percent = round(percent*100, 2) 59 | t_s = get_size(total_size) 60 | try: 61 | b_s = get_size(bytes_so_far, t_s.split(" ")[1]) 62 | except: 63 | b_s = get_size(bytes_so_far) 64 | perc_str = " {:.2f}%".format(percent) 65 | bar_width = (TERMINAL_WIDTH // 3)-len(perc_str) 66 | progress = "=" * int(bar_width * (percent/100)) 67 | sys.stdout.write("\r\033[K{}/{} | {}{}{}{}{}".format( 68 | b_s, 69 | t_s, 70 | progress, 71 | " " * (bar_width-len(progress)), 72 | perc_str, 73 | speed, 74 | remaining 75 | )) 76 | else: 77 | b_s = get_size(bytes_so_far) 78 | sys.stdout.write("\r\033[K{}{}".format(b_s, speed)) 79 | sys.stdout.flush() 80 | # Now we gather the next packet 81 | try: 82 | packet = queue.get(timeout=update_interval) 83 | # Packets should be formatted as a tuple of 84 | # (timestamp, len(bytes_downloaded)) 85 | # If "DONE" is passed, we assume the download 86 | # finished - and bail 87 | if packet == "DONE": 88 | print("") # Jump to the next line 89 | return 90 | # Append our packet to the list and ensure we're not 91 | # beyond our max. 92 | # Only check max if it's > 0 93 | packets.append(packet) 94 | if max_packets > 0: 95 | packets = packets[-max_packets:] 96 | # Increment our bytes so far as well 97 | bytes_so_far += packet[1] 98 | except q.Empty: 99 | # Didn't get anything - reset the speed 100 | # and packets 101 | packets = [] 102 | speed = " | 0 B/s" 103 | remaining = " | ?? left" if total_size > 0 else "" 104 | except KeyboardInterrupt: 105 | print("") # Jump to the next line 106 | return 107 | # If we have packets and it's time for an update, process 108 | # the info. 109 | update_check = time.time() 110 | if packets and update_check - last_update >= update_interval: 111 | last_update = update_check # Refresh our update timestamp 112 | speed = " | ?? B/s" 113 | if len(packets) > 1: 114 | # Let's calculate the amount downloaded over how long 115 | try: 116 | first,last = packets[0][0],packets[-1][0] 117 | chunks = sum([float(x[1]) for x in packets]) 118 | t = last-first 119 | assert t >= 0 120 | bytes_speed = 1. / t * chunks 121 | speed = " | {}/s".format(get_size(bytes_speed,round_to=1)) 122 | # Get our remaining time 123 | if total_size > 0: 124 | seconds_left = (total_size-bytes_so_far) / bytes_speed 125 | days = seconds_left // 86400 126 | hours = (seconds_left - (days*86400)) // 3600 127 | mins = (seconds_left - (days*86400) - (hours*3600)) // 60 128 | secs = seconds_left - (days*86400) - (hours*3600) - (mins*60) 129 | if days > 99 or bytes_speed == 0: 130 | remaining = " | ?? left" 131 | else: 132 | remaining = " | {}{:02d}:{:02d}:{:02d} left".format( 133 | "{}:".format(int(days)) if days else "", 134 | int(hours), 135 | int(mins), 136 | int(round(secs)) 137 | ) 138 | except: 139 | pass 140 | # Clear the packets so we don't reuse the same ones 141 | packets = [] 142 | 143 | class Downloader: 144 | 145 | def __init__(self,**kwargs): 146 | self.ua = kwargs.get("useragent",{"User-Agent":"Mozilla"}) 147 | self.chunk = 1048576 # 1024 x 1024 i.e. 1MiB 148 | if os.name=="nt": os.system("color") # Initialize cmd for ANSI escapes 149 | # Provide reasonable default logic to workaround macOS CA file handling 150 | cafile = ssl.get_default_verify_paths().openssl_cafile 151 | try: 152 | # If default OpenSSL CA file does not exist, use that from certifi 153 | if not os.path.exists(cafile): 154 | import certifi 155 | cafile = certifi.where() 156 | self.ssl_context = ssl.create_default_context(cafile=cafile) 157 | except: 158 | # None of the above worked, disable certificate verification for now 159 | self.ssl_context = ssl._create_unverified_context() 160 | return 161 | 162 | def _decode(self, value, encoding="utf-8", errors="ignore"): 163 | # Helper method to only decode if bytes type 164 | if sys.version_info >= (3,0) and isinstance(value, bytes): 165 | return value.decode(encoding,errors) 166 | return value 167 | 168 | def _update_main_name(self): 169 | # Windows running python 2 seems to have issues with multiprocessing 170 | # if the case of the main script's name is incorrect: 171 | # e.g. Downloader.py vs downloader.py 172 | # 173 | # To work around this, we try to scrape for the correct case if 174 | # possible. 175 | try: 176 | path = os.path.abspath(sys.modules["__main__"].__file__) 177 | except AttributeError as e: 178 | # This likely means we're running from the interpreter 179 | # directly 180 | return None 181 | if not os.path.isfile(path): 182 | return None 183 | # Get the file name and folder path 184 | name = os.path.basename(path).lower() 185 | fldr = os.path.dirname(path) 186 | # Walk the files in the folder until we find our 187 | # name - then steal its case and update that path 188 | for f in os.listdir(fldr): 189 | if f.lower() == name: 190 | # Got it 191 | new_path = os.path.join(fldr,f) 192 | sys.modules["__main__"].__file__ = new_path 193 | return new_path 194 | # If we got here, it wasn't found 195 | return None 196 | 197 | def _get_headers(self, headers = None): 198 | # Fall back on the default ua if none provided 199 | target = headers if isinstance(headers,dict) else self.ua 200 | new_headers = {} 201 | # Shallow copy to prevent changes to the headers 202 | # overriding the original 203 | for k in target: 204 | new_headers[k] = target[k] 205 | return new_headers 206 | 207 | def open_url(self, url, headers = None): 208 | headers = self._get_headers(headers) 209 | # Wrap up the try/except block so we don't have to do this for each function 210 | try: 211 | response = urlopen(Request(url, headers=headers), context=self.ssl_context) 212 | except Exception as e: 213 | # No fixing this - bail 214 | return None 215 | return response 216 | 217 | def get_size(self, *args, **kwargs): 218 | return get_size(*args,**kwargs) 219 | 220 | def get_string(self, url, progress = True, headers = None, expand_gzip = True): 221 | response = self.get_bytes(url,progress,headers,expand_gzip) 222 | if response is None: return None 223 | return self._decode(response) 224 | 225 | def get_bytes(self, url, progress = True, headers = None, expand_gzip = True): 226 | response = self.open_url(url, headers) 227 | if response is None: return None 228 | try: total_size = int(response.headers['Content-Length']) 229 | except: total_size = -1 230 | chunk_so_far = b"" 231 | packets = queue = process = None 232 | if progress: 233 | # Make sure our vars are initialized 234 | packets = [] if progress else None 235 | queue = multiprocessing.Queue() 236 | # Create the multiprocess and start it 237 | process = multiprocessing.Process( 238 | target=_process_hook, 239 | args=(queue,total_size) 240 | ) 241 | process.daemon = True 242 | # Filthy hack for earlier python versions on Windows 243 | if os.name == "nt" and hasattr(multiprocessing,"forking"): 244 | self._update_main_name() 245 | process.start() 246 | try: 247 | while True: 248 | chunk = response.read(self.chunk) 249 | if progress: 250 | # Add our items to the queue 251 | queue.put((time.time(),len(chunk))) 252 | if not chunk: break 253 | chunk_so_far += chunk 254 | finally: 255 | # Close the response whenever we're done 256 | response.close() 257 | if expand_gzip and response.headers.get("Content-Encoding","unknown").lower() == "gzip": 258 | fileobj = BytesIO(chunk_so_far) 259 | gfile = gzip.GzipFile(fileobj=fileobj) 260 | return gfile.read() 261 | if progress: 262 | # Finalize the queue and wait 263 | queue.put("DONE") 264 | process.join() 265 | return chunk_so_far 266 | 267 | def stream_to_file(self, url, file_path, progress = True, headers = None, ensure_size_if_present = True, allow_resume = False): 268 | response = self.open_url(url, headers) 269 | if response is None: return None 270 | bytes_so_far = 0 271 | try: total_size = int(response.headers['Content-Length']) 272 | except: total_size = -1 273 | packets = queue = process = None 274 | mode = "wb" 275 | if allow_resume and os.path.isfile(file_path) and total_size != -1: 276 | # File exists, we're resuming and have a target size. Check the 277 | # local file size. 278 | current_size = os.stat(file_path).st_size 279 | if current_size == total_size: 280 | # File is already complete - return the path 281 | return file_path 282 | elif current_size < total_size: 283 | response.close() 284 | # File is not complete - seek to our current size 285 | bytes_so_far = current_size 286 | mode = "ab" # Append 287 | # We also need to try creating a new request 288 | # in order to pass our range header 289 | new_headers = self._get_headers(headers) 290 | # Get the start byte, 0-indexed 291 | byte_string = "bytes={}-".format(current_size) 292 | new_headers["Range"] = byte_string 293 | response = self.open_url(url, new_headers) 294 | if response is None: return None 295 | if progress: 296 | # Make sure our vars are initialized 297 | packets = [] if progress else None 298 | queue = multiprocessing.Queue() 299 | # Create the multiprocess and start it 300 | process = multiprocessing.Process( 301 | target=_process_hook, 302 | args=(queue,total_size,bytes_so_far) 303 | ) 304 | process.daemon = True 305 | # Filthy hack for earlier python versions on Windows 306 | if os.name == "nt" and hasattr(multiprocessing,"forking"): 307 | self._update_main_name() 308 | process.start() 309 | with open(file_path,mode) as f: 310 | try: 311 | while True: 312 | chunk = response.read(self.chunk) 313 | bytes_so_far += len(chunk) 314 | if progress: 315 | # Add our items to the queue 316 | queue.put((time.time(),len(chunk))) 317 | if not chunk: break 318 | f.write(chunk) 319 | finally: 320 | # Close the response whenever we're done 321 | response.close() 322 | if progress: 323 | # Finalize the queue and wait 324 | queue.put("DONE") 325 | process.join() 326 | if ensure_size_if_present and total_size != -1: 327 | # We're verifying size - make sure we got what we asked for 328 | if bytes_so_far != total_size: 329 | return None # We didn't - imply it failed 330 | return file_path if os.path.exists(file_path) else None 331 | -------------------------------------------------------------------------------- /Scripts/plist.py: -------------------------------------------------------------------------------- 1 | ### ### 2 | # Imports # 3 | ### ### 4 | 5 | import datetime, os, plistlib, struct, sys, itertools, binascii 6 | from io import BytesIO 7 | 8 | if sys.version_info < (3,0): 9 | # Force use of StringIO instead of cStringIO as the latter 10 | # has issues with Unicode strings 11 | from StringIO import StringIO 12 | else: 13 | from io import StringIO 14 | 15 | try: 16 | basestring # Python 2 17 | unicode 18 | except NameError: 19 | basestring = str # Python 3 20 | unicode = str 21 | 22 | try: 23 | FMT_XML = plistlib.FMT_XML 24 | FMT_BINARY = plistlib.FMT_BINARY 25 | except AttributeError: 26 | FMT_XML = "FMT_XML" 27 | FMT_BINARY = "FMT_BINARY" 28 | 29 | ### ### 30 | # Helper Methods # 31 | ### ### 32 | 33 | def wrap_data(value): 34 | if not _check_py3(): return plistlib.Data(value) 35 | return value 36 | 37 | def extract_data(value): 38 | if not _check_py3() and isinstance(value,plistlib.Data): return value.data 39 | return value 40 | 41 | def _check_py3(): 42 | return sys.version_info >= (3, 0) 43 | 44 | def _is_binary(fp): 45 | if isinstance(fp, basestring): 46 | return fp.startswith(b"bplist00") 47 | header = fp.read(32) 48 | fp.seek(0) 49 | return header[:8] == b'bplist00' 50 | 51 | def _seek_past_whitespace(fp): 52 | offset = 0 53 | while True: 54 | byte = fp.read(1) 55 | if not byte: 56 | # End of file, reset offset and bail 57 | offset = 0 58 | break 59 | if not byte.isspace(): 60 | # Found our first non-whitespace character 61 | break 62 | offset += 1 63 | # Seek to the first non-whitespace char 64 | fp.seek(offset) 65 | return offset 66 | 67 | ### ### 68 | # Deprecated Functions - Remapped # 69 | ### ### 70 | 71 | def readPlist(pathOrFile): 72 | if not isinstance(pathOrFile, basestring): 73 | return load(pathOrFile) 74 | with open(pathOrFile, "rb") as f: 75 | return load(f) 76 | 77 | def writePlist(value, pathOrFile): 78 | if not isinstance(pathOrFile, basestring): 79 | return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False) 80 | with open(pathOrFile, "wb") as f: 81 | return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False) 82 | 83 | ### ### 84 | # Remapped Functions # 85 | ### ### 86 | 87 | def load(fp, fmt=None, use_builtin_types=None, dict_type=dict): 88 | if _is_binary(fp): 89 | use_builtin_types = False if use_builtin_types is None else use_builtin_types 90 | try: 91 | p = _BinaryPlistParser(use_builtin_types=use_builtin_types, dict_type=dict_type) 92 | except: 93 | # Python 3.9 removed use_builtin_types 94 | p = _BinaryPlistParser(dict_type=dict_type) 95 | return p.parse(fp) 96 | elif _check_py3(): 97 | offset = _seek_past_whitespace(fp) 98 | use_builtin_types = True if use_builtin_types is None else use_builtin_types 99 | # We need to monkey patch this to allow for hex integers - code taken/modified from 100 | # https://github.com/python/cpython/blob/3.8/Lib/plistlib.py 101 | if fmt is None: 102 | header = fp.read(32) 103 | fp.seek(offset) 104 | for info in plistlib._FORMATS.values(): 105 | if info['detect'](header): 106 | P = info['parser'] 107 | break 108 | else: 109 | raise plistlib.InvalidFileException() 110 | else: 111 | P = plistlib._FORMATS[fmt]['parser'] 112 | try: 113 | p = P(use_builtin_types=use_builtin_types, dict_type=dict_type) 114 | except: 115 | # Python 3.9 removed use_builtin_types 116 | p = P(dict_type=dict_type) 117 | if isinstance(p,plistlib._PlistParser): 118 | # Monkey patch! 119 | def end_integer(): 120 | d = p.get_data() 121 | value = int(d,16) if d.lower().startswith("0x") else int(d) 122 | if -1 << 63 <= value < 1 << 64: 123 | p.add_object(value) 124 | else: 125 | raise OverflowError("Integer overflow at line {}".format(p.parser.CurrentLineNumber)) 126 | def end_data(): 127 | try: 128 | p.add_object(plistlib._decode_base64(p.get_data())) 129 | except Exception as e: 130 | raise Exception("Data error at line {}: {}".format(p.parser.CurrentLineNumber,e)) 131 | p.end_integer = end_integer 132 | p.end_data = end_data 133 | return p.parse(fp) 134 | else: 135 | offset = _seek_past_whitespace(fp) 136 | # Is not binary - assume a string - and try to load 137 | # We avoid using readPlistFromString() as that uses 138 | # cStringIO and fails when Unicode strings are detected 139 | # Don't subclass - keep the parser local 140 | from xml.parsers.expat import ParserCreate 141 | # Create a new PlistParser object - then we need to set up 142 | # the values and parse. 143 | p = plistlib.PlistParser() 144 | parser = ParserCreate() 145 | parser.StartElementHandler = p.handleBeginElement 146 | parser.EndElementHandler = p.handleEndElement 147 | parser.CharacterDataHandler = p.handleData 148 | # We also need to monkey patch this to allow for other dict_types, hex int support 149 | # proper line output for data errors, and for unicode string decoding 150 | def begin_dict(attrs): 151 | d = dict_type() 152 | p.addObject(d) 153 | p.stack.append(d) 154 | def end_integer(): 155 | d = p.getData() 156 | value = int(d,16) if d.lower().startswith("0x") else int(d) 157 | if -1 << 63 <= value < 1 << 64: 158 | p.addObject(value) 159 | else: 160 | raise OverflowError("Integer overflow at line {}".format(parser.CurrentLineNumber)) 161 | def end_data(): 162 | try: 163 | p.addObject(plistlib.Data.fromBase64(p.getData())) 164 | except Exception as e: 165 | raise Exception("Data error at line {}: {}".format(parser.CurrentLineNumber,e)) 166 | def end_string(): 167 | d = p.getData() 168 | if isinstance(d,unicode): 169 | d = d.encode("utf-8") 170 | p.addObject(d) 171 | p.begin_dict = begin_dict 172 | p.end_integer = end_integer 173 | p.end_data = end_data 174 | p.end_string = end_string 175 | if isinstance(fp, unicode): 176 | # Encode unicode -> string; use utf-8 for safety 177 | fp = fp.encode("utf-8") 178 | if isinstance(fp, basestring): 179 | # It's a string - let's wrap it up 180 | fp = StringIO(fp) 181 | # Parse it 182 | parser.ParseFile(fp) 183 | return p.root 184 | 185 | def loads(value, fmt=None, use_builtin_types=None, dict_type=dict): 186 | if _check_py3() and isinstance(value, basestring): 187 | # If it's a string - encode it 188 | value = value.encode() 189 | try: 190 | return load(BytesIO(value),fmt=fmt,use_builtin_types=use_builtin_types,dict_type=dict_type) 191 | except: 192 | # Python 3.9 removed use_builtin_types 193 | return load(BytesIO(value),fmt=fmt,dict_type=dict_type) 194 | 195 | def dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False): 196 | if fmt == FMT_BINARY: 197 | # Assume binary at this point 198 | writer = _BinaryPlistWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys) 199 | writer.write(value) 200 | elif fmt == FMT_XML: 201 | if _check_py3(): 202 | plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) 203 | else: 204 | # We need to monkey patch a bunch here too in order to avoid auto-sorting 205 | # of keys 206 | writer = plistlib.PlistWriter(fp) 207 | def writeDict(d): 208 | if d: 209 | writer.beginElement("dict") 210 | items = sorted(d.items()) if sort_keys else d.items() 211 | for key, value in items: 212 | if not isinstance(key, basestring): 213 | if skipkeys: 214 | continue 215 | raise TypeError("keys must be strings") 216 | writer.simpleElement("key", key) 217 | writer.writeValue(value) 218 | writer.endElement("dict") 219 | else: 220 | writer.simpleElement("dict") 221 | writer.writeDict = writeDict 222 | writer.writeln("") 223 | writer.writeValue(value) 224 | writer.writeln("") 225 | else: 226 | # Not a proper format 227 | raise ValueError("Unsupported format: {}".format(fmt)) 228 | 229 | def dumps(value, fmt=FMT_XML, skipkeys=False, sort_keys=True): 230 | # We avoid using writePlistToString() as that uses 231 | # cStringIO and fails when Unicode strings are detected 232 | f = BytesIO() if _check_py3() else StringIO() 233 | dump(value, f, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys) 234 | value = f.getvalue() 235 | if _check_py3(): 236 | value = value.decode("utf-8") 237 | return value 238 | 239 | ### ### 240 | # Binary Plist Stuff For Py2 # 241 | ### ### 242 | 243 | # From the python 3 plistlib.py source: https://github.com/python/cpython/blob/3.11/Lib/plistlib.py 244 | # Tweaked to function on both Python 2 and 3 245 | 246 | class UID: 247 | def __init__(self, data): 248 | if not isinstance(data, int): 249 | raise TypeError("data must be an int") 250 | # It seems Apple only uses 32-bit unsigned ints for UIDs. Although the comment in 251 | # CoreFoundation's CFBinaryPList.c detailing the binary plist format theoretically 252 | # allows for 64-bit UIDs, most functions in the same file use 32-bit unsigned ints, 253 | # with the sole function hinting at 64-bits appearing to be a leftover from copying 254 | # and pasting integer handling code internally, and this code has not changed since 255 | # it was added. (In addition, code in CFPropertyList.c to handle CF$UID also uses a 256 | # 32-bit unsigned int.) 257 | # 258 | # if data >= 1 << 64: 259 | # raise ValueError("UIDs cannot be >= 2**64") 260 | if data >= 1 << 32: 261 | raise ValueError("UIDs cannot be >= 2**32 (4294967296)") 262 | if data < 0: 263 | raise ValueError("UIDs must be positive") 264 | self.data = data 265 | 266 | def __index__(self): 267 | return self.data 268 | 269 | def __repr__(self): 270 | return "%s(%s)" % (self.__class__.__name__, repr(self.data)) 271 | 272 | def __reduce__(self): 273 | return self.__class__, (self.data,) 274 | 275 | def __eq__(self, other): 276 | if not isinstance(other, UID): 277 | return NotImplemented 278 | return self.data == other.data 279 | 280 | def __hash__(self): 281 | return hash(self.data) 282 | 283 | class InvalidFileException (ValueError): 284 | def __init__(self, message="Invalid file"): 285 | ValueError.__init__(self, message) 286 | 287 | _BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'} 288 | 289 | _undefined = object() 290 | 291 | class _BinaryPlistParser: 292 | """ 293 | Read or write a binary plist file, following the description of the binary 294 | format. Raise InvalidFileException in case of error, otherwise return the 295 | root object. 296 | see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c 297 | """ 298 | def __init__(self, use_builtin_types, dict_type): 299 | self._use_builtin_types = use_builtin_types 300 | self._dict_type = dict_type 301 | 302 | def parse(self, fp): 303 | try: 304 | # The basic file format: 305 | # HEADER 306 | # object... 307 | # refid->offset... 308 | # TRAILER 309 | self._fp = fp 310 | self._fp.seek(-32, os.SEEK_END) 311 | trailer = self._fp.read(32) 312 | if len(trailer) != 32: 313 | raise InvalidFileException() 314 | ( 315 | offset_size, self._ref_size, num_objects, top_object, 316 | offset_table_offset 317 | ) = struct.unpack('>6xBBQQQ', trailer) 318 | self._fp.seek(offset_table_offset) 319 | self._object_offsets = self._read_ints(num_objects, offset_size) 320 | self._objects = [_undefined] * num_objects 321 | return self._read_object(top_object) 322 | 323 | except (OSError, IndexError, struct.error, OverflowError, 324 | UnicodeDecodeError): 325 | raise InvalidFileException() 326 | 327 | def _get_size(self, tokenL): 328 | """ return the size of the next object.""" 329 | if tokenL == 0xF: 330 | m = self._fp.read(1)[0] 331 | if not _check_py3(): 332 | m = ord(m) 333 | m = m & 0x3 334 | s = 1 << m 335 | f = '>' + _BINARY_FORMAT[s] 336 | return struct.unpack(f, self._fp.read(s))[0] 337 | 338 | return tokenL 339 | 340 | def _read_ints(self, n, size): 341 | data = self._fp.read(size * n) 342 | if size in _BINARY_FORMAT: 343 | return struct.unpack('>' + _BINARY_FORMAT[size] * n, data) 344 | else: 345 | if not size or len(data) != size * n: 346 | raise InvalidFileException() 347 | return tuple(int(binascii.hexlify(data[i: i + size]),16) 348 | for i in range(0, size * n, size)) 349 | '''return tuple(int.from_bytes(data[i: i + size], 'big') 350 | for i in range(0, size * n, size))''' 351 | 352 | def _read_refs(self, n): 353 | return self._read_ints(n, self._ref_size) 354 | 355 | def _read_object(self, ref): 356 | """ 357 | read the object by reference. 358 | May recursively read sub-objects (content of an array/dict/set) 359 | """ 360 | result = self._objects[ref] 361 | if result is not _undefined: 362 | return result 363 | 364 | offset = self._object_offsets[ref] 365 | self._fp.seek(offset) 366 | token = self._fp.read(1)[0] 367 | if not _check_py3(): 368 | token = ord(token) 369 | tokenH, tokenL = token & 0xF0, token & 0x0F 370 | 371 | if token == 0x00: # \x00 or 0x00 372 | result = None 373 | 374 | elif token == 0x08: # \x08 or 0x08 375 | result = False 376 | 377 | elif token == 0x09: # \x09 or 0x09 378 | result = True 379 | 380 | # The referenced source code also mentions URL (0x0c, 0x0d) and 381 | # UUID (0x0e), but neither can be generated using the Cocoa libraries. 382 | 383 | elif token == 0x0f: # \x0f or 0x0f 384 | result = b'' 385 | 386 | elif tokenH == 0x10: # int 387 | result = int(binascii.hexlify(self._fp.read(1 << tokenL)),16) 388 | if tokenL >= 3: # Signed - adjust 389 | result = result-((result & 0x8000000000000000) << 1) 390 | 391 | elif token == 0x22: # real 392 | result = struct.unpack('>f', self._fp.read(4))[0] 393 | 394 | elif token == 0x23: # real 395 | result = struct.unpack('>d', self._fp.read(8))[0] 396 | 397 | elif token == 0x33: # date 398 | f = struct.unpack('>d', self._fp.read(8))[0] 399 | # timestamp 0 of binary plists corresponds to 1/1/2001 400 | # (year of Mac OS X 10.0), instead of 1/1/1970. 401 | result = (datetime.datetime(2001, 1, 1) + 402 | datetime.timedelta(seconds=f)) 403 | 404 | elif tokenH == 0x40: # data 405 | s = self._get_size(tokenL) 406 | if self._use_builtin_types or not hasattr(plistlib, "Data"): 407 | result = self._fp.read(s) 408 | else: 409 | result = plistlib.Data(self._fp.read(s)) 410 | 411 | elif tokenH == 0x50: # ascii string 412 | s = self._get_size(tokenL) 413 | result = self._fp.read(s).decode('ascii') 414 | result = result 415 | 416 | elif tokenH == 0x60: # unicode string 417 | s = self._get_size(tokenL) 418 | result = self._fp.read(s * 2).decode('utf-16be') 419 | 420 | elif tokenH == 0x80: # UID 421 | # used by Key-Archiver plist files 422 | result = UID(int(binascii.hexlify(self._fp.read(1 + tokenL)),16)) 423 | 424 | elif tokenH == 0xA0: # array 425 | s = self._get_size(tokenL) 426 | obj_refs = self._read_refs(s) 427 | result = [] 428 | self._objects[ref] = result 429 | result.extend(self._read_object(x) for x in obj_refs) 430 | 431 | # tokenH == 0xB0 is documented as 'ordset', but is not actually 432 | # implemented in the Apple reference code. 433 | 434 | # tokenH == 0xC0 is documented as 'set', but sets cannot be used in 435 | # plists. 436 | 437 | elif tokenH == 0xD0: # dict 438 | s = self._get_size(tokenL) 439 | key_refs = self._read_refs(s) 440 | obj_refs = self._read_refs(s) 441 | result = self._dict_type() 442 | self._objects[ref] = result 443 | for k, o in zip(key_refs, obj_refs): 444 | key = self._read_object(k) 445 | if hasattr(plistlib, "Data") and isinstance(key, plistlib.Data): 446 | key = key.data 447 | result[key] = self._read_object(o) 448 | 449 | else: 450 | raise InvalidFileException() 451 | 452 | self._objects[ref] = result 453 | return result 454 | 455 | def _count_to_size(count): 456 | if count < 1 << 8: 457 | return 1 458 | 459 | elif count < 1 << 16: 460 | return 2 461 | 462 | elif count < 1 << 32: 463 | return 4 464 | 465 | else: 466 | return 8 467 | 468 | _scalars = (str, int, float, datetime.datetime, bytes) 469 | 470 | class _BinaryPlistWriter (object): 471 | def __init__(self, fp, sort_keys, skipkeys): 472 | self._fp = fp 473 | self._sort_keys = sort_keys 474 | self._skipkeys = skipkeys 475 | 476 | def write(self, value): 477 | 478 | # Flattened object list: 479 | self._objlist = [] 480 | 481 | # Mappings from object->objectid 482 | # First dict has (type(object), object) as the key, 483 | # second dict is used when object is not hashable and 484 | # has id(object) as the key. 485 | self._objtable = {} 486 | self._objidtable = {} 487 | 488 | # Create list of all objects in the plist 489 | self._flatten(value) 490 | 491 | # Size of object references in serialized containers 492 | # depends on the number of objects in the plist. 493 | num_objects = len(self._objlist) 494 | self._object_offsets = [0]*num_objects 495 | self._ref_size = _count_to_size(num_objects) 496 | 497 | self._ref_format = _BINARY_FORMAT[self._ref_size] 498 | 499 | # Write file header 500 | self._fp.write(b'bplist00') 501 | 502 | # Write object list 503 | for obj in self._objlist: 504 | self._write_object(obj) 505 | 506 | # Write refnum->object offset table 507 | top_object = self._getrefnum(value) 508 | offset_table_offset = self._fp.tell() 509 | offset_size = _count_to_size(offset_table_offset) 510 | offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects 511 | self._fp.write(struct.pack(offset_format, *self._object_offsets)) 512 | 513 | # Write trailer 514 | sort_version = 0 515 | trailer = ( 516 | sort_version, offset_size, self._ref_size, num_objects, 517 | top_object, offset_table_offset 518 | ) 519 | self._fp.write(struct.pack('>5xBBBQQQ', *trailer)) 520 | 521 | def _flatten(self, value): 522 | # First check if the object is in the object table, not used for 523 | # containers to ensure that two subcontainers with the same contents 524 | # will be serialized as distinct values. 525 | if isinstance(value, _scalars): 526 | if (type(value), value) in self._objtable: 527 | return 528 | 529 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 530 | if (type(value.data), value.data) in self._objtable: 531 | return 532 | 533 | elif id(value) in self._objidtable: 534 | return 535 | 536 | # Add to objectreference map 537 | refnum = len(self._objlist) 538 | self._objlist.append(value) 539 | if isinstance(value, _scalars): 540 | self._objtable[(type(value), value)] = refnum 541 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 542 | self._objtable[(type(value.data), value.data)] = refnum 543 | else: 544 | self._objidtable[id(value)] = refnum 545 | 546 | # And finally recurse into containers 547 | if isinstance(value, dict): 548 | keys = [] 549 | values = [] 550 | items = value.items() 551 | if self._sort_keys: 552 | items = sorted(items) 553 | 554 | for k, v in items: 555 | if not isinstance(k, basestring): 556 | if self._skipkeys: 557 | continue 558 | raise TypeError("keys must be strings") 559 | keys.append(k) 560 | values.append(v) 561 | 562 | for o in itertools.chain(keys, values): 563 | self._flatten(o) 564 | 565 | elif isinstance(value, (list, tuple)): 566 | for o in value: 567 | self._flatten(o) 568 | 569 | def _getrefnum(self, value): 570 | if isinstance(value, _scalars): 571 | return self._objtable[(type(value), value)] 572 | elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): 573 | return self._objtable[(type(value.data), value.data)] 574 | else: 575 | return self._objidtable[id(value)] 576 | 577 | def _write_size(self, token, size): 578 | if size < 15: 579 | self._fp.write(struct.pack('>B', token | size)) 580 | 581 | elif size < 1 << 8: 582 | self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size)) 583 | 584 | elif size < 1 << 16: 585 | self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size)) 586 | 587 | elif size < 1 << 32: 588 | self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size)) 589 | 590 | else: 591 | self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size)) 592 | 593 | def _write_object(self, value): 594 | ref = self._getrefnum(value) 595 | self._object_offsets[ref] = self._fp.tell() 596 | if value is None: 597 | self._fp.write(b'\x00') 598 | 599 | elif value is False: 600 | self._fp.write(b'\x08') 601 | 602 | elif value is True: 603 | self._fp.write(b'\x09') 604 | 605 | elif isinstance(value, int): 606 | if value < 0: 607 | try: 608 | self._fp.write(struct.pack('>Bq', 0x13, value)) 609 | except struct.error: 610 | raise OverflowError(value) # from None 611 | elif value < 1 << 8: 612 | self._fp.write(struct.pack('>BB', 0x10, value)) 613 | elif value < 1 << 16: 614 | self._fp.write(struct.pack('>BH', 0x11, value)) 615 | elif value < 1 << 32: 616 | self._fp.write(struct.pack('>BL', 0x12, value)) 617 | elif value < 1 << 63: 618 | self._fp.write(struct.pack('>BQ', 0x13, value)) 619 | elif value < 1 << 64: 620 | self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True)) 621 | else: 622 | raise OverflowError(value) 623 | 624 | elif isinstance(value, float): 625 | self._fp.write(struct.pack('>Bd', 0x23, value)) 626 | 627 | elif isinstance(value, datetime.datetime): 628 | f = (value - datetime.datetime(2001, 1, 1)).total_seconds() 629 | self._fp.write(struct.pack('>Bd', 0x33, f)) 630 | 631 | elif (_check_py3() and isinstance(value, (bytes, bytearray))) or (hasattr(plistlib, "Data") and isinstance(value, plistlib.Data)): 632 | if not isinstance(value, (bytes, bytearray)): 633 | value = value.data # Unpack it 634 | self._write_size(0x40, len(value)) 635 | self._fp.write(value) 636 | 637 | elif isinstance(value, basestring): 638 | try: 639 | t = value.encode('ascii') 640 | self._write_size(0x50, len(value)) 641 | except UnicodeEncodeError: 642 | t = value.encode('utf-16be') 643 | self._write_size(0x60, len(t) // 2) 644 | self._fp.write(t) 645 | 646 | elif isinstance(value, UID) or (hasattr(plistlib,"UID") and isinstance(value, plistlib.UID)): 647 | if value.data < 0: 648 | raise ValueError("UIDs must be positive") 649 | elif value.data < 1 << 8: 650 | self._fp.write(struct.pack('>BB', 0x80, value)) 651 | elif value.data < 1 << 16: 652 | self._fp.write(struct.pack('>BH', 0x81, value)) 653 | elif value.data < 1 << 32: 654 | self._fp.write(struct.pack('>BL', 0x83, value)) 655 | # elif value.data < 1 << 64: 656 | # self._fp.write(struct.pack('>BQ', 0x87, value)) 657 | else: 658 | raise OverflowError(value) 659 | 660 | elif isinstance(value, (list, tuple)): 661 | refs = [self._getrefnum(o) for o in value] 662 | s = len(refs) 663 | self._write_size(0xA0, s) 664 | self._fp.write(struct.pack('>' + self._ref_format * s, *refs)) 665 | 666 | elif isinstance(value, dict): 667 | keyRefs, valRefs = [], [] 668 | 669 | if self._sort_keys: 670 | rootItems = sorted(value.items()) 671 | else: 672 | rootItems = value.items() 673 | 674 | for k, v in rootItems: 675 | if not isinstance(k, basestring): 676 | if self._skipkeys: 677 | continue 678 | raise TypeError("keys must be strings") 679 | keyRefs.append(self._getrefnum(k)) 680 | valRefs.append(self._getrefnum(v)) 681 | 682 | s = len(keyRefs) 683 | self._write_size(0xD0, s) 684 | self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs)) 685 | self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs)) 686 | 687 | else: 688 | raise TypeError(value) 689 | -------------------------------------------------------------------------------- /Scripts/utils.py: -------------------------------------------------------------------------------- 1 | import sys, os, time, re, json, datetime, ctypes, subprocess 2 | 3 | if os.name == "nt": 4 | # Windows 5 | import msvcrt 6 | else: 7 | # Not Windows \o/ 8 | import select 9 | 10 | class Utils: 11 | 12 | def __init__(self, name = "Python Script"): 13 | self.name = name 14 | # Init our colors before we need to print anything 15 | cwd = os.getcwd() 16 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 17 | if os.path.exists("colors.json"): 18 | self.colors_dict = json.load(open("colors.json")) 19 | else: 20 | self.colors_dict = {} 21 | os.chdir(cwd) 22 | 23 | def check_admin(self): 24 | # Returns whether or not we're admin 25 | try: 26 | is_admin = os.getuid() == 0 27 | except AttributeError: 28 | is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 29 | return is_admin 30 | 31 | def elevate(self, file): 32 | # Runs the passed file as admin 33 | if self.check_admin(): 34 | return 35 | if os.name == "nt": 36 | ctypes.windll.shell32.ShellExecuteW(None, "runas", '"{}"'.format(sys.executable), '"{}"'.format(file), None, 1) 37 | else: 38 | try: 39 | p = subprocess.Popen(["which", "sudo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 40 | c = p.communicate()[0].decode("utf-8", "ignore").replace("\n", "") 41 | os.execv(c, [ sys.executable, 'python'] + sys.argv) 42 | except: 43 | exit(1) 44 | 45 | def compare_versions(self, vers1, vers2, **kwargs): 46 | # Helper method to compare ##.## strings 47 | # 48 | # vers1 < vers2 = True 49 | # vers1 = vers2 = None 50 | # vers1 > vers2 = False 51 | 52 | # Sanitize the pads 53 | pad = str(kwargs.get("pad", "")) 54 | sep = str(kwargs.get("separator", ".")) 55 | 56 | ignore_case = kwargs.get("ignore_case", True) 57 | 58 | # Cast as strings 59 | vers1 = str(vers1) 60 | vers2 = str(vers2) 61 | 62 | if ignore_case: 63 | vers1 = vers1.lower() 64 | vers2 = vers2.lower() 65 | 66 | # Split and pad lists 67 | v1_parts, v2_parts = self.pad_length(vers1.split(sep), vers2.split(sep)) 68 | 69 | # Iterate and compare 70 | for i in range(len(v1_parts)): 71 | # Remove non-numeric 72 | v1 = ''.join(c.lower() for c in v1_parts[i] if c.isalnum()) 73 | v2 = ''.join(c.lower() for c in v2_parts[i] if c.isalnum()) 74 | # Equalize the lengths 75 | v1, v2 = self.pad_length(v1, v2) 76 | # Compare 77 | if str(v1) < str(v2): 78 | return True 79 | elif str(v1) > str(v2): 80 | return False 81 | # Never differed - return None, must be equal 82 | return None 83 | 84 | def pad_length(self, var1, var2, pad = "0"): 85 | # Pads the vars on the left side to make them equal length 86 | pad = "0" if len(str(pad)) < 1 else str(pad)[0] 87 | if not type(var1) == type(var2): 88 | # Type mismatch! Just return what we got 89 | return (var1, var2) 90 | if len(var1) < len(var2): 91 | if type(var1) is list: 92 | var1.extend([str(pad) for x in range(len(var2) - len(var1))]) 93 | else: 94 | var1 = "{}{}".format((pad*(len(var2)-len(var1))), var1) 95 | elif len(var2) < len(var1): 96 | if type(var2) is list: 97 | var2.extend([str(pad) for x in range(len(var1) - len(var2))]) 98 | else: 99 | var2 = "{}{}".format((pad*(len(var1)-len(var2))), var2) 100 | return (var1, var2) 101 | 102 | def check_path(self, path): 103 | # Let's loop until we either get a working path, or no changes 104 | test_path = path 105 | last_path = None 106 | while True: 107 | # Bail if we've looped at least once and the path didn't change 108 | if last_path != None and last_path == test_path: return None 109 | last_path = test_path 110 | # Check if we stripped everything out 111 | if not len(test_path): return None 112 | # Check if we have a valid path 113 | if os.path.exists(test_path): 114 | return os.path.abspath(test_path) 115 | # Check for quotes 116 | if test_path[0] == test_path[-1] and test_path[0] in ('"',"'"): 117 | test_path = test_path[1:-1] 118 | continue 119 | # Check for a tilde and expand if needed 120 | if test_path[0] == "~": 121 | tilde_expanded = os.path.expanduser(test_path) 122 | if tilde_expanded != test_path: 123 | # Got a change 124 | test_path = tilde_expanded 125 | continue 126 | # Let's check for spaces - strip from the left first, then the right 127 | if test_path[0] in (" ","\t"): 128 | test_path = test_path[1:] 129 | continue 130 | if test_path[-1] in (" ","\t"): 131 | test_path = test_path[:-1] 132 | continue 133 | # Maybe we have escapes to handle? 134 | test_path = "\\".join([x.replace("\\", "") for x in test_path.split("\\\\")]) 135 | 136 | def grab(self, prompt, **kwargs): 137 | # Takes a prompt, a default, and a timeout and shows it with that timeout 138 | # returning the result 139 | timeout = kwargs.get("timeout",0) 140 | default = kwargs.get("default","") 141 | # If we don't have a timeout - then skip the timed sections 142 | if timeout <= 0: 143 | try: 144 | if sys.version_info >= (3, 0): 145 | return input(prompt) 146 | else: 147 | return str(raw_input(prompt)) 148 | except EOFError: 149 | return default 150 | # Write our prompt 151 | sys.stdout.write(prompt) 152 | sys.stdout.flush() 153 | if os.name == "nt": 154 | start_time = time.time() 155 | i = '' 156 | while True: 157 | if msvcrt.kbhit(): 158 | c = msvcrt.getche() 159 | if ord(c) == 13: # enter_key 160 | break 161 | elif ord(c) >= 32: # space_char 162 | i += c.decode() if sys.version_info >= (3,0) and isinstance(c,bytes) else c 163 | else: 164 | time.sleep(0.02) # Delay for 20ms to prevent CPU workload 165 | if len(i) == 0 and (time.time() - start_time) > timeout: 166 | break 167 | else: 168 | i, o, e = select.select( [sys.stdin], [], [], timeout ) 169 | if i: 170 | i = sys.stdin.readline().strip() 171 | print('') # needed to move to next line 172 | if len(i) > 0: 173 | return i 174 | else: 175 | return default 176 | 177 | def cls(self): 178 | if os.name == "nt": 179 | os.system("cls") 180 | elif os.environ.get("TERM"): 181 | os.system("clear") 182 | 183 | def cprint(self, message, **kwargs): 184 | strip_colors = kwargs.get("strip_colors", False) 185 | if os.name == "nt": 186 | strip_colors = True 187 | reset = u"\u001b[0m" 188 | # Requires sys import 189 | for c in self.colors: 190 | if strip_colors: 191 | message = message.replace(c["find"], "") 192 | else: 193 | message = message.replace(c["find"], c["replace"]) 194 | if strip_colors: 195 | return message 196 | sys.stdout.write(message) 197 | print(reset) 198 | 199 | # Needs work to resize the string if color chars exist 200 | '''# Header drawing method 201 | def head(self, text = None, width = 55): 202 | if text == None: 203 | text = self.name 204 | self.cls() 205 | print(" {}".format("#"*width)) 206 | len_text = self.cprint(text, strip_colors=True) 207 | mid_len = int(round(width/2-len(len_text)/2)-2) 208 | middle = " #{}{}{}#".format(" "*mid_len, len_text, " "*((width - mid_len - len(len_text))-2)) 209 | if len(middle) > width+1: 210 | # Get the difference 211 | di = len(middle) - width 212 | # Add the padding for the ...# 213 | di += 3 214 | # Trim the string 215 | middle = middle[:-di] 216 | newlen = len(middle) 217 | middle += "...#" 218 | find_list = [ c["find"] for c in self.colors ] 219 | 220 | # Translate colored string to len 221 | middle = middle.replace(len_text, text + self.rt_color) # always reset just in case 222 | self.cprint(middle) 223 | print("#"*width)''' 224 | 225 | # Header drawing method 226 | def head(self, text = None, width = 55): 227 | if text == None: 228 | text = self.name 229 | self.cls() 230 | print(" {}".format("#"*width)) 231 | mid_len = int(round(width/2-len(text)/2)-2) 232 | middle = " #{}{}{}#".format(" "*mid_len, text, " "*((width - mid_len - len(text))-2)) 233 | if len(middle) > width+1: 234 | # Get the difference 235 | di = len(middle) - width 236 | # Add the padding for the ...# 237 | di += 3 238 | # Trim the string 239 | middle = middle[:-di] + "...#" 240 | print(middle) 241 | print("#"*width) 242 | 243 | def resize(self, width, height): 244 | print('\033[8;{};{}t'.format(height, width)) 245 | 246 | def custom_quit(self): 247 | self.head() 248 | print("by CorpNewt\n") 249 | print("Thanks for testing it out, for bugs/comments/complaints") 250 | print("send me a message on Reddit, or check out my GitHub:\n") 251 | print("www.reddit.com/u/corpnewt") 252 | print("www.github.com/corpnewt\n") 253 | # Get the time and wish them a good morning, afternoon, evening, and night 254 | hr = datetime.datetime.now().time().hour 255 | if hr > 3 and hr < 12: 256 | print("Have a nice morning!\n\n") 257 | elif hr >= 12 and hr < 17: 258 | print("Have a nice afternoon!\n\n") 259 | elif hr >= 17 and hr < 21: 260 | print("Have a nice evening!\n\n") 261 | else: 262 | print("Have a nice night!\n\n") 263 | exit(0) 264 | --------------------------------------------------------------------------------