├── bin ├── linux │ └── put_save3ds_fuse_here ├── darwin │ └── save3ds_fuse ├── win32 │ └── save3ds_fuse.exe ├── README └── Cargo.lock ├── requirements.txt ├── requirements-win32.txt ├── title.db.gz ├── TaskbarLib.tlb ├── finalize ├── README.md ├── data │ └── basetik.bin ├── source │ └── main.c └── Makefile ├── .gitignore ├── extras └── windows-quickstart.txt ├── .pylintrc ├── windows-install-dependencies.py ├── setup-cxfreeze.py ├── make-standalone.bat ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── ci-gui.py └── custominstall.py /bin/linux/put_save3ds_fuse_here: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | events==0.4 2 | pyctr>=0.4,<0.7 3 | -------------------------------------------------------------------------------- /requirements-win32.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | comtypes==1.1.10 3 | -------------------------------------------------------------------------------- /title.db.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaveamac/custom-install/HEAD/title.db.gz -------------------------------------------------------------------------------- /TaskbarLib.tlb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaveamac/custom-install/HEAD/TaskbarLib.tlb -------------------------------------------------------------------------------- /finalize/README.md: -------------------------------------------------------------------------------- 1 | # custom-install-finalize 2 | Finishes the process after using custom-install. 3 | -------------------------------------------------------------------------------- /bin/darwin/save3ds_fuse: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaveamac/custom-install/HEAD/bin/darwin/save3ds_fuse -------------------------------------------------------------------------------- /bin/win32/save3ds_fuse.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaveamac/custom-install/HEAD/bin/win32/save3ds_fuse.exe -------------------------------------------------------------------------------- /finalize/data/basetik.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaveamac/custom-install/HEAD/finalize/data/basetik.bin -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | bin/linux/save3ds_fuse 3 | cstins/ 4 | testing-class.py 5 | 6 | # macOS 7 | .DS_Store 8 | ._* 9 | 10 | # Python 11 | venv/ 12 | **/__pycache__/ 13 | *.pyc 14 | 15 | # JetBrains 16 | .idea/ 17 | ======= 18 | 19 | *.pyc 20 | /build/ 21 | /dist/ 22 | /custom-install-finalize.3dsx 23 | -------------------------------------------------------------------------------- /extras/windows-quickstart.txt: -------------------------------------------------------------------------------- 1 | Run ci-gui to bring up the custom-install gui. 2 | Select your SD card root, boot9, seeddb, and movable.sed files. 3 | In some cases these will be automatically selected for you. 4 | 5 | Add the CIA files and click "Start install". 6 | 7 | Once it's finished, start up the homebrew launcher and run custom-install-finalize to finish the process. 8 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | ignored-classes=Events 3 | 4 | [MASTER] 5 | disable=missing-docstring, 6 | invalid-name, 7 | line-too-long, 8 | bad-continuation, 9 | consider-using-enumerate, 10 | trailing-whitespace, 11 | wrong-import-order, 12 | subprocess-run-check, 13 | singleton-comparison, 14 | attribute-defined-outside-init, 15 | fixme, 16 | redefined-outer-name, 17 | multiple-statements, 18 | bare-except -------------------------------------------------------------------------------- /windows-install-dependencies.py: -------------------------------------------------------------------------------- 1 | # This is meant to be double-clicked from File Explorer. 2 | 3 | # This doesn't import pip as a module in case the way it's executed changes, which it has in the past. 4 | # Instead we call it like we would in the command line. 5 | 6 | from subprocess import run 7 | from os.path import dirname, join 8 | from sys import executable 9 | 10 | root_dir = dirname(__file__) 11 | 12 | run([executable, '-m', 'pip', 'install', '--user', '-r', join(root_dir, 'requirements-win32.txt')]) 13 | input('Press enter to close') 14 | -------------------------------------------------------------------------------- /setup-cxfreeze.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from cx_Freeze import setup, Executable 3 | 4 | if sys.platform == 'win32': 5 | executables = [ 6 | Executable('ci-gui.py', target_name='ci-gui-console'), 7 | Executable('ci-gui.py', target_name='ci-gui', base='Win32GUI'), 8 | ] 9 | else: 10 | executables = [ 11 | Executable('ci-gui.py', target_name='ci-gui'), 12 | ] 13 | 14 | setup( 15 | name = "ci-gui", 16 | version = "2.1b4", 17 | description = "Installs a title directly to an SD card for the Nintendo 3DS", 18 | executables = executables 19 | ) 20 | -------------------------------------------------------------------------------- /bin/README: -------------------------------------------------------------------------------- 1 | save3ds_fuse for win32 and darwin built with commit 568b0597b17da0c8cfbd345bab27176cd84bd883 2 | in repository https://github.com/wwylele/save3ds 3 | 4 | win32 binary built on Windows 10, version 21H2 64-bit with `cargo build --release --target=i686-pc-windows-msvc`. 5 | 6 | darwin binary built on macOS 12.2 with: 7 | * `cargo build --target=aarch64-apple-darwin --no-default-features --release` 8 | * `cargo build --target=x86_64-apple-darwin --no-default-features --release` 9 | * Then a universal binary is built: `lipo -create -output save3ds_fuse-universal2 target/aarch64-apple-darwin/release/save3ds_fuse target/x86_64-apple-darwin/release/save3ds_fuse` 10 | 11 | linux binary must be provided by the user. 12 | -------------------------------------------------------------------------------- /make-standalone.bat: -------------------------------------------------------------------------------- 1 | mkdir build 2 | mkdir dist 3 | python setup-cxfreeze.py build_exe --build-exe=build\custom-install-standalone 4 | mkdir build\custom-install-standalone\bin 5 | copy TaskbarLib.tlb build\custom-install-standalone 6 | copy bin\win32\save3ds_fuse.exe build\custom-install-standalone\bin 7 | copy bin\README build\custom-install-standalone\bin 8 | copy custom-install-finalize.3dsx build\custom-install-standalone 9 | copy title.db.gz build\custom-install-standalone 10 | copy extras\windows-quickstart.txt build\custom-install-standalone 11 | copy extras\run_with_cmd.bat build\custom-install-standalone 12 | copy LICENSE.md build\custom-install-standalone 13 | python -m zipfile -c dist\custom-install-standalone.zip build\custom-install-standalone 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## This is my personal project 2 | 3 | I make this project in my free time whenever I feel like it. I make no promises about reading issues or pull requests on a timely basis, or that I will fix certain issues or merge pull requests (soon or ever). 4 | 5 | If you are making a significant addition and you intend for it to be implemented in my repository, you should talk to me first, because putting it in my repo means I have to maintain it. Please keep in mind the above paragraph. Maybe keep your own fork if you need something. 6 | 7 | ## No AI-generated content 8 | 9 | Absolutely NO content generated by "artificial intelligence" for **__any reason whatsoever__** (including issues, pull requests, and code). You're wasting my time and yours. If you try to and I find it, I will delete it and likely block you. 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2021 Ian Burgwin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)]() ![Releases](https://img.shields.io/github/downloads/ihaveamac/custom-install/total.svg) 2 | 3 | # custom-install 4 | Installs a title directly to an SD card for the Nintendo 3DS. Originally created late June 2019. 5 | 6 | ## Summary 7 | 8 | ### Windows standalone 9 | 10 | 1. [Dump boot9.bin and movable.sed](https://ihaveamac.github.io/dump.html) from a 3DS system. 11 | 2. Download the [latest releases](https://github.com/ihaveamac/custom-install/releases). 12 | 3. Extract and run ci-gui. Read `windows-quickstart.txt`. 13 | 14 | ### With installed Python 15 | Note for Windows users: Enabling "Add Python 3.X to PATH" is **NOT** required! Python is installed with the `py` launcher by default. 16 | 17 | 1. [Dump boot9.bin and movable.sed](https://ihaveamac.github.io/dump.html) from a 3DS system. 18 | 2. Download the repo ([zip link](https://github.com/ihaveamac/custom-install/archive/safe-install.zip) or `git clone`) 19 | 3. Install the packages: 20 | * Windows: Double-click `windows-install-dependencies.py` 21 | * Alternate manual method: `py -3 -m pip install --user -r requirements-win32.txt` 22 | * macOS/Linux: `python3 -m pip install --user -r requirements.txt` 23 | 4. Run `custominstall.py` with boot9.bin, movable.sed, path to the SD root, and CIA files to install (see Usage section). 24 | 5. Download and use [custom-install-finalize](https://github.com/ihaveamac/custom-install/releases) on the 3DS system to finish the install. 25 | 26 | ## Setup 27 | Linux users must build [wwylele/save3ds](https://github.com/wwylele/save3ds) and place `save3ds_fuse` in `bin/linux`. Install [rust using rustup](https://www.rust-lang.org/tools/install), then compile with: `cargo build --release --no-default-features`. The compiled binary is located in `target/release/save3ds_fuse`, copy it to `bin/linux`. 28 | 29 | movable.sed is required and can be provided with `-m` or `--movable`. 30 | 31 | boot9 is needed: 32 | * `-b` or `--boot9` argument (if set) 33 | * `BOOT9_PATH` environment variable (if set) 34 | * `%APPDATA%\3ds\boot9.bin` (Windows-specific) 35 | * `~/Library/Application Support/3ds/boot9.bin` (macOS-specific) 36 | * `~/.3ds/boot9.bin` 37 | * `~/3ds/boot9.bin` 38 | 39 | A [SeedDB](https://github.com/ihaveamac/3DS-rom-tools/wiki/SeedDB-list) is needed for newer games (2015+) that use seeds. 40 | SeedDB is checked in order of: 41 | * `-s` or `--seeddb` argument (if set) 42 | * `SEEDDB_PATH` environment variable (if set) 43 | * `%APPDATA%\3ds\seeddb.bin` (Windows-specific) 44 | * `~/Library/Application Support/3ds/seeddb.bin` (macOS-specific) 45 | * `~/.3ds/seeddb.bin` 46 | * `~/3ds/seeddb.bin` 47 | 48 | ## custom-install-finalize 49 | custom-install-finalize installs a ticket, plus a seed if required. This is required for the title to appear and function. 50 | 51 | This can be built as most 3DS homebrew projects [with devkitARM](https://www.3dbrew.org/wiki/Setting_up_Development_Environment). 52 | 53 | ## Usage 54 | Use `-h` to view arguments. 55 | 56 | Examples: 57 | ``` 58 | py -3 custominstall.py -b boot9.bin -m movable.sed --sd E:\ file.cia file2.cia 59 | python3 custominstall.py -b boot9.bin -m movable.sed --sd /Volumes/GM9SD file.cia file2.cia 60 | python3 custominstall.py -b boot9.bin -m movable.sed --sd /media/GM9SD file.cia file2.cia 61 | ``` 62 | 63 | ## GUI 64 | A GUI is provided to make the process easier. 65 | 66 | ### GUI Setup 67 | Linux users may need to install a Tk package: 68 | - Ubuntu/Debian: `sudo apt install python3-tk` 69 | - Manjaro/Arch: `sudo pacman -S tk` 70 | 71 | Install the requirements listed in "Summary", then run `ci-gui.py`. 72 | 73 | ## Development 74 | 75 | ### Building Windows standalone 76 | 77 | Using a 32-bit version of Python is recommended to build a version to be distributed. 78 | 79 | A [virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment) is recommended to isolate the packages from system directories. The build script `make-standalone.bat` assumes that the dependencies are in PATH. 80 | 81 | Install the dependencies, plus cx-Freeze. In a virtual environment, the specific Python version doesn't need to be requested. 82 | ```batch 83 | pip install cx-freeze -r requirements-win32.txt 84 | ``` 85 | 86 | Copy `custom-install-finalize.3dsx` to the project root, this will be copied to the build directory and included in the final archive. 87 | 88 | Run `make-standalone.bat`. This will run cxfreeze and make a standalone version at `dist\custom-install-standalone.zip` 89 | 90 | ## License/Credits 91 | [save3ds by wwylele](https://github.com/wwylele/save3ds) is used to interact with the Title Database (details in `bin/README`). 92 | 93 | Thanks to @nek0bit for redesigning `custominstall.py` to work as a module, and for implementing an earlier GUI. 94 | 95 | Thanks to @LyfeOnEdge from the [brewtools Discord](https://brewtools.dev) for designing the second version of the GUI. Special thanks to CrafterPika and archbox for testing. 96 | 97 | Thanks to @BpyH64 for [researching how to generate the cmacs](https://github.com/d0k3/GodMode9/issues/340#issuecomment-487916606). 98 | -------------------------------------------------------------------------------- /finalize/source/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include <3ds.h> 6 | 7 | #include "basetik_bin.h" 8 | 9 | #define CIFINISH_PATH "/cifinish.bin" 10 | 11 | // 0x10 12 | struct finish_db_header { 13 | u8 magic[8]; 14 | u32 version; 15 | u32 title_count; 16 | }; 17 | 18 | // 0x30 19 | struct finish_db_entry_v1 { 20 | u64 title_id; 21 | u8 common_key_index; // unused by this program 22 | bool has_seed; 23 | u8 magic[6]; // "TITLE" and a null byte 24 | u8 title_key[0x10]; // unused by this program 25 | u8 seed[0x10]; 26 | }; 27 | 28 | // 0x20 29 | // this one was accidential since I mixed up the order of the members in the script 30 | // and the finalize program, but a lot of users probably used the bad one so I need 31 | // to support this anyway. 32 | struct finish_db_entry_v2 { 33 | u8 magic[6]; // "TITLE" and a null byte 34 | u64 title_id; 35 | bool has_seed; 36 | u8 padding; 37 | u8 seed[0x10]; 38 | } __attribute__((packed)); 39 | 40 | // 0x20 41 | struct finish_db_entry_v3 { 42 | u8 magic[6]; // "TITLE" and a null byte 43 | bool has_seed; 44 | u64 title_id; 45 | u8 seed[0x10]; 46 | }; 47 | 48 | // 0x350 49 | struct ticket_dumb { 50 | u8 unused1[0x1DC]; 51 | u64 title_id_be; 52 | u8 unused2[0x16C]; 53 | } __attribute__((packed)); 54 | 55 | // the 3 versions are put into this struct 56 | struct finish_db_entry_final { 57 | bool has_seed; 58 | u64 title_id; 59 | u8 seed[0x10]; 60 | }; 61 | 62 | // from FBI: 63 | // https://github.com/Steveice10/FBI/blob/6e3a28e4b674e0d7a6f234b0419c530b358957db/source/core/http.c#L440-L453 64 | static Result FSUSER_AddSeed(u64 titleId, const void* seed) { 65 | u32 *cmdbuf = getThreadCommandBuffer(); 66 | 67 | cmdbuf[0] = 0x087A0180; 68 | cmdbuf[1] = (u32) (titleId & 0xFFFFFFFF); 69 | cmdbuf[2] = (u32) (titleId >> 32); 70 | memcpy(&cmdbuf[3], seed, 16); 71 | 72 | Result ret = 0; 73 | if(R_FAILED(ret = svcSendSyncRequest(*fsGetSessionHandle()))) return ret; 74 | 75 | ret = cmdbuf[1]; 76 | return ret; 77 | } 78 | 79 | int load_cifinish(char* path, struct finish_db_entry_final **entries) 80 | { 81 | FILE *fp; 82 | struct finish_db_header header; 83 | 84 | struct finish_db_entry_v1 v1; 85 | struct finish_db_entry_v2 v2; 86 | struct finish_db_entry_v3 v3; 87 | 88 | struct finish_db_entry_final *tmp; 89 | 90 | int i; 91 | size_t read; 92 | 93 | printf("Reading %s...\n", path); 94 | fp = fopen(path, "rb"); 95 | if (!fp) 96 | { 97 | printf("Failed to open file. Does it exist?\n"); 98 | return -1; 99 | } 100 | 101 | fread(&header, sizeof(header), 1, fp); 102 | 103 | if (memcmp(header.magic, "CIFINISH", 8)) 104 | { 105 | printf("CIFINISH magic not found.\n"); 106 | goto fail; 107 | } 108 | 109 | printf("CIFINISH version: %lu\n", header.version); 110 | 111 | if (header.version > 3) 112 | { 113 | printf("This version of custom-install-finalize is\n"); 114 | printf(" too old. Please update to a new release.\n"); 115 | goto fail; 116 | } 117 | 118 | *entries = calloc(header.title_count, sizeof(struct finish_db_entry_final)); 119 | if (!*entries) { 120 | printf("Couldn't allocate memory.\n"); 121 | printf("This should never happen.\n"); 122 | goto fail; 123 | } 124 | tmp = *entries; 125 | 126 | if (header.version == 1) 127 | { 128 | for (i = 0; i < header.title_count; i++) 129 | { 130 | read = fread(&v1, sizeof(v1), 1, fp); 131 | if (read != 1) 132 | { 133 | printf("Couldn't read a full entry.\n"); 134 | printf(" Is the file corrupt?\n"); 135 | goto fail; 136 | } 137 | 138 | if (memcmp(v1.magic, "TITLE", 6)) 139 | { 140 | printf("Couldn't find TITLE magic for entry.\n"); 141 | printf(" Is the file corrupt?\n"); 142 | goto fail; 143 | } 144 | tmp[i].has_seed = v1.has_seed; 145 | tmp[i].title_id = v1.title_id; 146 | memcpy(tmp[i].seed, v1.seed, 16); 147 | } 148 | } else if (header.version == 2) { 149 | for (i = 0; i < header.title_count; i++) 150 | { 151 | read = fread(&v2, sizeof(v2), 1, fp); 152 | if (read != 1) 153 | { 154 | printf("Couldn't read a full entry.\n"); 155 | printf(" Is the file corrupt?\n"); 156 | goto fail; 157 | } 158 | 159 | if (memcmp(v2.magic, "TITLE", 6)) 160 | { 161 | printf("Couldn't find TITLE magic for entry.\n"); 162 | printf(" Is the file corrupt?\n"); 163 | goto fail; 164 | } 165 | tmp[i].has_seed = v2.has_seed; 166 | tmp[i].title_id = v2.title_id; 167 | memcpy(tmp[i].seed, v2.seed, 16); 168 | } 169 | } else if (header.version == 3) { 170 | for (i = 0; i < header.title_count; i++) 171 | { 172 | read = fread(&v3, sizeof(v3), 1, fp); 173 | if (read != 1) 174 | { 175 | printf("Couldn't read a full entry.\n"); 176 | printf(" Is the file corrupt?\n"); 177 | goto fail; 178 | } 179 | 180 | if (memcmp(v3.magic, "TITLE", 6)) 181 | { 182 | printf("Couldn't find TITLE magic for entry.\n"); 183 | printf(" Is the file corrupt?\n"); 184 | goto fail; 185 | } 186 | tmp[i].has_seed = v3.has_seed; 187 | tmp[i].title_id = v3.title_id; 188 | memcpy(tmp[i].seed, v3.seed, 16); 189 | } 190 | } 191 | 192 | fclose(fp); 193 | return header.title_count; 194 | 195 | fail: 196 | fclose(fp); 197 | return -1; 198 | } 199 | 200 | Result check_title_exist(u64 title_id, u64 *ticket_ids, u32 ticket_ids_length, u64 *title_ids, u32 title_ids_length) 201 | { 202 | Result ret = -2; 203 | 204 | for (u32 i = 0; i < ticket_ids_length; i++) 205 | { 206 | if (ticket_ids[i] == title_id) 207 | { 208 | ret++; 209 | break; 210 | } 211 | } 212 | 213 | for (u32 i = 0; i < title_ids_length; i++) 214 | { 215 | if (title_ids[i] == title_id) 216 | { 217 | ret++; 218 | break; 219 | } 220 | } 221 | 222 | return ret; 223 | } 224 | 225 | void finalize_install(void) 226 | { 227 | Result res; 228 | Handle ticketHandle; 229 | struct ticket_dumb ticket_buf; 230 | struct finish_db_entry_final *entries = NULL; 231 | int title_count; 232 | 233 | u32 titles_read; 234 | u32 tickets_read; 235 | 236 | res = AM_GetTitleCount(MEDIATYPE_SD, &titles_read); 237 | 238 | if (R_FAILED(res)) 239 | { 240 | return; 241 | } 242 | 243 | res = AM_GetTicketCount(&tickets_read); 244 | 245 | if (R_FAILED(res)) 246 | { 247 | return; 248 | } 249 | 250 | u64 *installed_ticket_ids = malloc(sizeof(u64) * tickets_read ); 251 | u64 *installed_title_ids = malloc(sizeof(u64) * titles_read ); 252 | 253 | res = AM_GetTitleList(&titles_read, MEDIATYPE_SD, titles_read, installed_title_ids); 254 | 255 | if (R_FAILED(res)) 256 | { 257 | goto exit; 258 | } 259 | 260 | res = AM_GetTicketList(&tickets_read, tickets_read, 0, installed_ticket_ids); 261 | 262 | if (R_FAILED(res)) 263 | { 264 | goto exit; 265 | } 266 | 267 | title_count = load_cifinish(CIFINISH_PATH, &entries); 268 | 269 | if (title_count == -1) 270 | { 271 | goto exit; 272 | } 273 | else if (title_count == 0) 274 | { 275 | printf("No titles to finalize.\n"); 276 | goto exit; 277 | } 278 | 279 | memcpy(&ticket_buf, basetik_bin, basetik_bin_size); 280 | 281 | Result exist_res = 0; 282 | 283 | for (int i = 0; i < title_count; ++i) 284 | { 285 | exist_res = check_title_exist(entries[i].title_id, installed_ticket_ids, tickets_read, installed_title_ids, titles_read); 286 | 287 | if (R_SUCCEEDED(exist_res)) 288 | { 289 | printf("No need to finalize %016llx, skipping...\n", entries[i].title_id); 290 | continue; 291 | } 292 | 293 | printf("Finalizing %016llx...\n", entries[i].title_id); 294 | 295 | ticket_buf.title_id_be = __builtin_bswap64(entries[i].title_id); 296 | 297 | res = AM_InstallTicketBegin(&ticketHandle); 298 | if (R_FAILED(res)) 299 | { 300 | printf("Failed to begin ticket install: %08lx\n", res); 301 | AM_InstallTicketAbort(ticketHandle); 302 | goto exit; 303 | } 304 | 305 | res = FSFILE_Write(ticketHandle, NULL, 0, &ticket_buf, sizeof(struct ticket_dumb), 0); 306 | if (R_FAILED(res)) 307 | { 308 | printf("Failed to write ticket: %08lx\n", res); 309 | AM_InstallTicketAbort(ticketHandle); 310 | goto exit; 311 | } 312 | 313 | res = AM_InstallTicketFinish(ticketHandle); 314 | if (R_FAILED(res)) 315 | { 316 | printf("Failed to finish ticket install: %08lx\n", res); 317 | AM_InstallTicketAbort(ticketHandle); 318 | goto exit; 319 | } 320 | 321 | if (entries[i].has_seed) 322 | { 323 | res = FSUSER_AddSeed(entries[i].title_id, entries[i].seed); 324 | if (R_FAILED(res)) 325 | { 326 | printf("Failed to install seed: %08lx\n", res); 327 | continue; 328 | } 329 | } 330 | } 331 | 332 | printf("Deleting %s...\n", CIFINISH_PATH); 333 | unlink(CIFINISH_PATH); 334 | 335 | exit: 336 | 337 | free(entries); 338 | free(installed_ticket_ids); 339 | free(installed_title_ids); 340 | return; 341 | } 342 | 343 | int main(int argc, char* argv[]) 344 | { 345 | amInit(); 346 | gfxInitDefault(); 347 | consoleInit(GFX_TOP, NULL); 348 | 349 | printf("custom-install-finalize v1.6\n"); 350 | 351 | finalize_install(); 352 | // print this at the end in case it gets pushed off the screen 353 | printf("\nRepository:\n"); 354 | printf(" https://github.com/ihaveamac/custom-install\n"); 355 | printf("\nPress START or B to exit.\n"); 356 | 357 | // Main loop 358 | while (aptMainLoop()) 359 | { 360 | gspWaitForVBlank(); 361 | gfxSwapBuffers(); 362 | hidScanInput(); 363 | 364 | // Your code goes here 365 | u32 kDown = hidKeysDown(); 366 | if (kDown & KEY_START || kDown & KEY_B) 367 | break; // break in order to return to hbmenu 368 | } 369 | 370 | gfxExit(); 371 | amExit(); 372 | return 0; 373 | } 374 | -------------------------------------------------------------------------------- /finalize/Makefile: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------------------------------- 2 | .SUFFIXES: 3 | #--------------------------------------------------------------------------------- 4 | 5 | ifeq ($(strip $(DEVKITARM)),) 6 | $(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") 7 | endif 8 | 9 | TOPDIR ?= $(CURDIR) 10 | include $(DEVKITARM)/3ds_rules 11 | 12 | #--------------------------------------------------------------------------------- 13 | # TARGET is the name of the output 14 | # BUILD is the directory where object files & intermediate files will be placed 15 | # SOURCES is a list of directories containing source code 16 | # DATA is a list of directories containing data files 17 | # INCLUDES is a list of directories containing header files 18 | # GRAPHICS is a list of directories containing graphics files 19 | # GFXBUILD is the directory where converted graphics files will be placed 20 | # If set to $(BUILD), it will statically link in the converted 21 | # files as if they were data files. 22 | # 23 | # NO_SMDH: if set to anything, no SMDH file is generated. 24 | # ROMFS is the directory which contains the RomFS, relative to the Makefile (Optional) 25 | # APP_TITLE is the name of the app stored in the SMDH file (Optional) 26 | # APP_DESCRIPTION is the description of the app stored in the SMDH file (Optional) 27 | # APP_AUTHOR is the author of the app stored in the SMDH file (Optional) 28 | # ICON is the filename of the icon (.png), relative to the project folder. 29 | # If not set, it attempts to use one of the following (in this order): 30 | # - .png 31 | # - icon.png 32 | # - /default_icon.png 33 | #--------------------------------------------------------------------------------- 34 | TARGET := custom-install-finalize 35 | BUILD := build 36 | SOURCES := source 37 | DATA := data 38 | INCLUDES := include 39 | GRAPHICS := gfx 40 | GFXBUILD := $(BUILD) 41 | #ROMFS := romfs 42 | #GFXBUILD := $(ROMFS)/gfx 43 | 44 | APP_TITLE := custom-install-finalize 45 | APP_DESCRIPTION := Finalize installation from custom-install. 46 | APP_AUTHOR := ihaveamac 47 | 48 | #--------------------------------------------------------------------------------- 49 | # options for code generation 50 | #--------------------------------------------------------------------------------- 51 | ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft 52 | 53 | CFLAGS := -g -Wall -O2 -mword-relocations \ 54 | -fomit-frame-pointer -ffunction-sections \ 55 | $(ARCH) 56 | 57 | CFLAGS += $(INCLUDE) -D__3DS__ 58 | 59 | CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 60 | 61 | ASFLAGS := -g $(ARCH) 62 | LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) 63 | 64 | LIBS := -lctru -lm 65 | 66 | #--------------------------------------------------------------------------------- 67 | # list of directories containing libraries, this must be the top level containing 68 | # include and lib 69 | #--------------------------------------------------------------------------------- 70 | LIBDIRS := $(CTRULIB) 71 | 72 | 73 | #--------------------------------------------------------------------------------- 74 | # no real need to edit anything past this point unless you need to add additional 75 | # rules for different file extensions 76 | #--------------------------------------------------------------------------------- 77 | ifneq ($(BUILD),$(notdir $(CURDIR))) 78 | #--------------------------------------------------------------------------------- 79 | 80 | export OUTPUT := $(CURDIR)/$(TARGET) 81 | export TOPDIR := $(CURDIR) 82 | 83 | export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ 84 | $(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir)) \ 85 | $(foreach dir,$(DATA),$(CURDIR)/$(dir)) 86 | 87 | export DEPSDIR := $(CURDIR)/$(BUILD) 88 | 89 | CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) 90 | CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) 91 | SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) 92 | PICAFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.v.pica))) 93 | SHLISTFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.shlist))) 94 | GFXFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.t3s))) 95 | BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) 96 | 97 | #--------------------------------------------------------------------------------- 98 | # use CXX for linking C++ projects, CC for standard C 99 | #--------------------------------------------------------------------------------- 100 | ifeq ($(strip $(CPPFILES)),) 101 | #--------------------------------------------------------------------------------- 102 | export LD := $(CC) 103 | #--------------------------------------------------------------------------------- 104 | else 105 | #--------------------------------------------------------------------------------- 106 | export LD := $(CXX) 107 | #--------------------------------------------------------------------------------- 108 | endif 109 | #--------------------------------------------------------------------------------- 110 | 111 | #--------------------------------------------------------------------------------- 112 | ifeq ($(GFXBUILD),$(BUILD)) 113 | #--------------------------------------------------------------------------------- 114 | export T3XFILES := $(GFXFILES:.t3s=.t3x) 115 | #--------------------------------------------------------------------------------- 116 | else 117 | #--------------------------------------------------------------------------------- 118 | export ROMFS_T3XFILES := $(patsubst %.t3s, $(GFXBUILD)/%.t3x, $(GFXFILES)) 119 | export T3XHFILES := $(patsubst %.t3s, $(BUILD)/%.h, $(GFXFILES)) 120 | #--------------------------------------------------------------------------------- 121 | endif 122 | #--------------------------------------------------------------------------------- 123 | 124 | export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) 125 | 126 | export OFILES_BIN := $(addsuffix .o,$(BINFILES)) \ 127 | $(PICAFILES:.v.pica=.shbin.o) $(SHLISTFILES:.shlist=.shbin.o) \ 128 | $(addsuffix .o,$(T3XFILES)) 129 | 130 | export OFILES := $(OFILES_BIN) $(OFILES_SOURCES) 131 | 132 | export HFILES := $(PICAFILES:.v.pica=_shbin.h) $(SHLISTFILES:.shlist=_shbin.h) \ 133 | $(addsuffix .h,$(subst .,_,$(BINFILES))) \ 134 | $(GFXFILES:.t3s=.h) 135 | 136 | export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ 137 | $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ 138 | -I$(CURDIR)/$(BUILD) 139 | 140 | export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) 141 | 142 | export _3DSXDEPS := $(if $(NO_SMDH),,$(OUTPUT).smdh) 143 | 144 | ifeq ($(strip $(ICON)),) 145 | icons := $(wildcard *.png) 146 | ifneq (,$(findstring $(TARGET).png,$(icons))) 147 | export APP_ICON := $(TOPDIR)/$(TARGET).png 148 | else 149 | ifneq (,$(findstring icon.png,$(icons))) 150 | export APP_ICON := $(TOPDIR)/icon.png 151 | endif 152 | endif 153 | else 154 | export APP_ICON := $(TOPDIR)/$(ICON) 155 | endif 156 | 157 | ifeq ($(strip $(NO_SMDH)),) 158 | export _3DSXFLAGS += --smdh=$(CURDIR)/$(TARGET).smdh 159 | endif 160 | 161 | ifneq ($(ROMFS),) 162 | export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS) 163 | endif 164 | 165 | .PHONY: all clean 166 | 167 | #--------------------------------------------------------------------------------- 168 | all: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) 169 | @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile 170 | 171 | $(BUILD): 172 | @mkdir -p $@ 173 | 174 | ifneq ($(GFXBUILD),$(BUILD)) 175 | $(GFXBUILD): 176 | @mkdir -p $@ 177 | endif 178 | 179 | ifneq ($(DEPSDIR),$(BUILD)) 180 | $(DEPSDIR): 181 | @mkdir -p $@ 182 | endif 183 | 184 | #--------------------------------------------------------------------------------- 185 | clean: 186 | @echo clean ... 187 | @rm -fr $(BUILD) $(TARGET).3dsx $(OUTPUT).smdh $(TARGET).elf $(GFXBUILD) 188 | 189 | #--------------------------------------------------------------------------------- 190 | $(GFXBUILD)/%.t3x $(BUILD)/%.h : %.t3s 191 | #--------------------------------------------------------------------------------- 192 | @echo $(notdir $<) 193 | @tex3ds -i $< -H $(BUILD)/$*.h -d $(DEPSDIR)/$*.d -o $(GFXBUILD)/$*.t3x 194 | 195 | #--------------------------------------------------------------------------------- 196 | else 197 | 198 | #--------------------------------------------------------------------------------- 199 | # main targets 200 | #--------------------------------------------------------------------------------- 201 | $(OUTPUT).3dsx : $(OUTPUT).elf $(_3DSXDEPS) 202 | 203 | $(OFILES_SOURCES) : $(HFILES) 204 | 205 | $(OUTPUT).elf : $(OFILES) 206 | 207 | #--------------------------------------------------------------------------------- 208 | # you need a rule like this for each extension you use as binary data 209 | #--------------------------------------------------------------------------------- 210 | %.bin.o %_bin.h : %.bin 211 | #--------------------------------------------------------------------------------- 212 | @echo $(notdir $<) 213 | @$(bin2o) 214 | 215 | #--------------------------------------------------------------------------------- 216 | .PRECIOUS : %.t3x 217 | #--------------------------------------------------------------------------------- 218 | %.t3x.o %_t3x.h : %.t3x 219 | #--------------------------------------------------------------------------------- 220 | @echo $(notdir $<) 221 | @$(bin2o) 222 | 223 | #--------------------------------------------------------------------------------- 224 | # rules for assembling GPU shaders 225 | #--------------------------------------------------------------------------------- 226 | define shader-as 227 | $(eval CURBIN := $*.shbin) 228 | $(eval DEPSFILE := $(DEPSDIR)/$*.shbin.d) 229 | echo "$(CURBIN).o: $< $1" > $(DEPSFILE) 230 | echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(CURBIN) | tr . _)`.h 231 | echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(CURBIN) | tr . _)`.h 232 | echo "extern const u32" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(CURBIN) | tr . _)`.h 233 | picasso -o $(CURBIN) $1 234 | bin2s $(CURBIN) | $(AS) -o $*.shbin.o 235 | endef 236 | 237 | %.shbin.o %_shbin.h : %.v.pica %.g.pica 238 | @echo $(notdir $^) 239 | @$(call shader-as,$^) 240 | 241 | %.shbin.o %_shbin.h : %.v.pica 242 | @echo $(notdir $<) 243 | @$(call shader-as,$<) 244 | 245 | %.shbin.o %_shbin.h : %.shlist 246 | @echo $(notdir $<) 247 | @$(call shader-as,$(foreach file,$(shell cat $<),$(dir $<)$(file))) 248 | 249 | #--------------------------------------------------------------------------------- 250 | %.t3x %.h : %.t3s 251 | #--------------------------------------------------------------------------------- 252 | @echo $(notdir $<) 253 | @tex3ds -i $< -H $*.h -d $*.d -o $*.t3x 254 | 255 | -include $(DEPSDIR)/*.d 256 | 257 | #--------------------------------------------------------------------------------------- 258 | endif 259 | #--------------------------------------------------------------------------------------- 260 | -------------------------------------------------------------------------------- /bin/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aes" 5 | version = "0.4.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | dependencies = [ 8 | "aes-soft 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 9 | "aesni 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 10 | "block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 11 | ] 12 | 13 | [[package]] 14 | name = "aes-soft" 15 | version = "0.4.0" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | dependencies = [ 18 | "block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 19 | "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 20 | "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 21 | ] 22 | 23 | [[package]] 24 | name = "aesni" 25 | version = "0.7.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | dependencies = [ 28 | "block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 29 | "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 30 | ] 31 | 32 | [[package]] 33 | name = "ahash" 34 | version = "0.2.18" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | dependencies = [ 37 | "const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", 38 | ] 39 | 40 | [[package]] 41 | name = "atty" 42 | version = "0.2.11" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | dependencies = [ 45 | "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", 46 | "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", 47 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 48 | ] 49 | 50 | [[package]] 51 | name = "autocfg" 52 | version = "0.1.7" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | 55 | [[package]] 56 | name = "autocfg" 57 | version = "1.0.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | 60 | [[package]] 61 | name = "block-buffer" 62 | version = "0.8.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | dependencies = [ 65 | "block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 66 | "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 67 | "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 68 | "generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)", 69 | ] 70 | 71 | [[package]] 72 | name = "block-cipher" 73 | version = "0.7.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | dependencies = [ 76 | "generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)", 77 | ] 78 | 79 | [[package]] 80 | name = "block-padding" 81 | version = "0.1.5" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | dependencies = [ 84 | "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 85 | ] 86 | 87 | [[package]] 88 | name = "byte-tools" 89 | version = "0.3.1" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | 92 | [[package]] 93 | name = "byte_struct" 94 | version = "0.6.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | dependencies = [ 97 | "byte_struct_derive 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 98 | "generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)", 99 | ] 100 | 101 | [[package]] 102 | name = "byte_struct_derive" 103 | version = "0.4.2" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | dependencies = [ 106 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 107 | "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", 108 | "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", 109 | ] 110 | 111 | [[package]] 112 | name = "byteorder" 113 | version = "1.3.4" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | 116 | [[package]] 117 | name = "cfg-if" 118 | version = "0.1.10" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | 121 | [[package]] 122 | name = "chrono" 123 | version = "0.4.11" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | dependencies = [ 126 | "num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", 127 | "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", 128 | "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", 129 | ] 130 | 131 | [[package]] 132 | name = "cmac" 133 | version = "0.3.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | dependencies = [ 136 | "block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 137 | "crypto-mac 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 138 | "dbl 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 139 | ] 140 | 141 | [[package]] 142 | name = "const-random" 143 | version = "0.1.8" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | dependencies = [ 146 | "const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", 147 | "proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)", 148 | ] 149 | 150 | [[package]] 151 | name = "const-random-macro" 152 | version = "0.1.8" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | dependencies = [ 155 | "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", 156 | "proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)", 157 | ] 158 | 159 | [[package]] 160 | name = "crypto-mac" 161 | version = "0.8.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | dependencies = [ 164 | "generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)", 165 | "subtle 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 166 | ] 167 | 168 | [[package]] 169 | name = "dbl" 170 | version = "0.3.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | dependencies = [ 173 | "generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)", 174 | ] 175 | 176 | [[package]] 177 | name = "digest" 178 | version = "0.9.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | dependencies = [ 181 | "generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)", 182 | ] 183 | 184 | [[package]] 185 | name = "fake-simd" 186 | version = "0.1.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | 189 | [[package]] 190 | name = "fuse" 191 | version = "0.3.1" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | dependencies = [ 194 | "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", 195 | "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 196 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 197 | "thread-scoped 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 198 | "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", 199 | ] 200 | 201 | [[package]] 202 | name = "generic-array" 203 | version = "0.14.2" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | dependencies = [ 206 | "typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", 207 | "version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", 208 | ] 209 | 210 | [[package]] 211 | name = "getopts" 212 | version = "0.2.21" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | dependencies = [ 215 | "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 216 | ] 217 | 218 | [[package]] 219 | name = "getrandom" 220 | version = "0.1.14" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | dependencies = [ 223 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 224 | "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", 225 | "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", 226 | ] 227 | 228 | [[package]] 229 | name = "hashbrown" 230 | version = "0.6.3" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | dependencies = [ 233 | "ahash 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", 234 | "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 235 | ] 236 | 237 | [[package]] 238 | name = "lazy_static" 239 | version = "0.2.11" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | 242 | [[package]] 243 | name = "libc" 244 | version = "0.2.71" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | 247 | [[package]] 248 | name = "libsave3ds" 249 | version = "0.1.0" 250 | dependencies = [ 251 | "aes 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 252 | "byte_struct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 253 | "cmac 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 254 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 255 | "lru 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 256 | "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", 257 | "sha2 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", 258 | ] 259 | 260 | [[package]] 261 | name = "log" 262 | version = "0.3.9" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | dependencies = [ 265 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 266 | ] 267 | 268 | [[package]] 269 | name = "log" 270 | version = "0.4.8" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | dependencies = [ 273 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 274 | ] 275 | 276 | [[package]] 277 | name = "lru" 278 | version = "0.5.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | dependencies = [ 281 | "hashbrown 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", 282 | ] 283 | 284 | [[package]] 285 | name = "num-integer" 286 | version = "0.1.43" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | dependencies = [ 289 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 290 | "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", 291 | ] 292 | 293 | [[package]] 294 | name = "num-traits" 295 | version = "0.2.12" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | dependencies = [ 298 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 299 | ] 300 | 301 | [[package]] 302 | name = "numtoa" 303 | version = "0.1.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | 306 | [[package]] 307 | name = "opaque-debug" 308 | version = "0.2.3" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | 311 | [[package]] 312 | name = "pkg-config" 313 | version = "0.3.17" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | 316 | [[package]] 317 | name = "ppv-lite86" 318 | version = "0.2.8" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | 321 | [[package]] 322 | name = "proc-macro-hack" 323 | version = "0.5.16" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | 326 | [[package]] 327 | name = "proc-macro2" 328 | version = "0.4.30" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | dependencies = [ 331 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 332 | ] 333 | 334 | [[package]] 335 | name = "quote" 336 | version = "0.6.13" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | dependencies = [ 339 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 340 | ] 341 | 342 | [[package]] 343 | name = "rand" 344 | version = "0.7.3" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | dependencies = [ 347 | "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", 348 | "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", 349 | "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 350 | "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 351 | "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 352 | ] 353 | 354 | [[package]] 355 | name = "rand_chacha" 356 | version = "0.2.2" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | dependencies = [ 359 | "ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 360 | "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 361 | ] 362 | 363 | [[package]] 364 | name = "rand_core" 365 | version = "0.5.1" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | dependencies = [ 368 | "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", 369 | ] 370 | 371 | [[package]] 372 | name = "rand_hc" 373 | version = "0.2.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | dependencies = [ 376 | "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 377 | ] 378 | 379 | [[package]] 380 | name = "redox_syscall" 381 | version = "0.1.56" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | 384 | [[package]] 385 | name = "redox_termios" 386 | version = "0.1.1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | dependencies = [ 389 | "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", 390 | ] 391 | 392 | [[package]] 393 | name = "save3ds_fuse" 394 | version = "0.1.0" 395 | dependencies = [ 396 | "fuse 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 397 | "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", 398 | "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", 399 | "libsave3ds 0.1.0", 400 | "stderrlog 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 401 | "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", 402 | ] 403 | 404 | [[package]] 405 | name = "sha2" 406 | version = "0.9.0" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | dependencies = [ 409 | "block-buffer 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 410 | "digest 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", 411 | "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 412 | "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 413 | ] 414 | 415 | [[package]] 416 | name = "stderrlog" 417 | version = "0.4.3" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | dependencies = [ 420 | "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 421 | "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", 422 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 423 | "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 424 | "thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 425 | ] 426 | 427 | [[package]] 428 | name = "subtle" 429 | version = "2.2.3" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | 432 | [[package]] 433 | name = "syn" 434 | version = "0.15.44" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | dependencies = [ 437 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 438 | "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", 439 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 440 | ] 441 | 442 | [[package]] 443 | name = "termcolor" 444 | version = "1.1.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | dependencies = [ 447 | "winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 448 | ] 449 | 450 | [[package]] 451 | name = "termion" 452 | version = "1.5.5" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | dependencies = [ 455 | "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", 456 | "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 457 | "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", 458 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 459 | ] 460 | 461 | [[package]] 462 | name = "thread-scoped" 463 | version = "1.0.2" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | 466 | [[package]] 467 | name = "thread_local" 468 | version = "0.3.4" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | dependencies = [ 471 | "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 472 | "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 473 | ] 474 | 475 | [[package]] 476 | name = "time" 477 | version = "0.1.43" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | dependencies = [ 480 | "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", 481 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 482 | ] 483 | 484 | [[package]] 485 | name = "typenum" 486 | version = "1.12.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | 489 | [[package]] 490 | name = "unicode-width" 491 | version = "0.1.7" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | 494 | [[package]] 495 | name = "unicode-xid" 496 | version = "0.1.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | 499 | [[package]] 500 | name = "unreachable" 501 | version = "1.0.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | dependencies = [ 504 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 505 | ] 506 | 507 | [[package]] 508 | name = "version_check" 509 | version = "0.9.2" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | 512 | [[package]] 513 | name = "void" 514 | version = "1.0.2" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | 517 | [[package]] 518 | name = "wasi" 519 | version = "0.9.0+wasi-snapshot-preview1" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | 522 | [[package]] 523 | name = "winapi" 524 | version = "0.3.8" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | dependencies = [ 527 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 528 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 529 | ] 530 | 531 | [[package]] 532 | name = "winapi-i686-pc-windows-gnu" 533 | version = "0.4.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | 536 | [[package]] 537 | name = "winapi-util" 538 | version = "0.1.5" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | dependencies = [ 541 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 542 | ] 543 | 544 | [[package]] 545 | name = "winapi-x86_64-pc-windows-gnu" 546 | version = "0.4.0" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | 549 | [metadata] 550 | "checksum aes 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f7001367fde4c768a19d1029f0a8be5abd9308e1119846d5bd9ad26297b8faf5" 551 | "checksum aes-soft 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4925647ee64e5056cf231608957ce7c81e12d6d6e316b9ce1404778cc1d35fa7" 552 | "checksum aesni 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d050d39b0b7688b3a3254394c3e30a9d66c41dcf9b05b0e2dbdc623f6505d264" 553 | "checksum ahash 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "6f33b5018f120946c1dcf279194f238a9f146725593ead1c08fa47ff22b0b5d3" 554 | "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" 555 | "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" 556 | "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 557 | "checksum block-buffer 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dbcf92448676f82bb7a334c58bbce8b0d43580fb5362a9d608b18879d12a3d31" 558 | "checksum block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10" 559 | "checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" 560 | "checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" 561 | "checksum byte_struct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bde2e17424d6d3042b950f39de519dfd398c2e08adb1402d3fc10232a17564e" 562 | "checksum byte_struct_derive 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7fb6eccde50afec044557d1f1b8776168b7040255390eefffb39fcfd1ab40b2e" 563 | "checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 564 | "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 565 | "checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" 566 | "checksum cmac 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9f8f8ba8b9640e29213f152015694e78208e601adf91c72b698460633b15715" 567 | "checksum const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" 568 | "checksum const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" 569 | "checksum crypto-mac 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" 570 | "checksum dbl 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2735145c3b9ba15f2d7a3ae8cdafcbc8c98a7bef7f62afe9d08bd99fbf7130de" 571 | "checksum digest 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 572 | "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" 573 | "checksum fuse 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80e57070510966bfef93662a81cb8aa2b1c7db0964354fa9921434f04b9e8660" 574 | "checksum generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980" 575 | "checksum getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 576 | "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 577 | "checksum hashbrown 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8e6073d0ca812575946eb5f35ff68dbe519907b25c42530389ff946dc84c6ead" 578 | "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" 579 | "checksum libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" 580 | "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" 581 | "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 582 | "checksum lru 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28e0c685219cd60e49a2796bba7e4fe6523e10daca4fd721e84e7f905093d60c" 583 | "checksum num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" 584 | "checksum num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" 585 | "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 586 | "checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" 587 | "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" 588 | "checksum ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" 589 | "checksum proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)" = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" 590 | "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" 591 | "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" 592 | "checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 593 | "checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 594 | "checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 595 | "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 596 | "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 597 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 598 | "checksum sha2 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "72377440080fd008550fe9b441e854e43318db116f90181eef92e9ae9aedab48" 599 | "checksum stderrlog 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32e5ee9b90a5452c570a0b0ac1c99ae9498db7e56e33d74366de7f2a7add7f25" 600 | "checksum subtle 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "502d53007c02d7605a05df1c1a73ee436952781653da5d0bf57ad608f66932c1" 601 | "checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" 602 | "checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 603 | "checksum termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" 604 | "checksum thread-scoped 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bcbb6aa301e5d3b0b5ef639c9a9c7e2f1c944f177b460c04dc24c69b1fa2bd99" 605 | "checksum thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1697c4b57aeeb7a536b647165a2825faddffb1d3bad386d507709bd51a90bb14" 606 | "checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 607 | "checksum typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" 608 | "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 609 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 610 | "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" 611 | "checksum version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 612 | "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 613 | "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 614 | "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 615 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 616 | "checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 617 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 618 | -------------------------------------------------------------------------------- /ci-gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is a part of custom-install.py. 4 | # 5 | # custom-install is copyright (c) 2019-2020 Ian Burgwin 6 | # This file is licensed under The MIT License (MIT). 7 | # You can find the full license text in LICENSE.md in the root of this project. 8 | 9 | from os import environ, scandir 10 | from os.path import abspath, basename, dirname, join, isfile 11 | import sys 12 | from threading import Thread, Lock 13 | from time import strftime 14 | from traceback import format_exception 15 | import tkinter as tk 16 | import tkinter.ttk as ttk 17 | import tkinter.filedialog as fd 18 | import tkinter.messagebox as mb 19 | from typing import TYPE_CHECKING 20 | 21 | from pyctr.crypto import MissingSeedError, CryptoEngine, load_seeddb 22 | from pyctr.crypto.engine import b9_paths 23 | from pyctr.util import config_dirs 24 | from pyctr.type.cdn import CDNError 25 | from pyctr.type.cia import CIAError 26 | from pyctr.type.tmd import TitleMetadataError 27 | 28 | from custominstall import CustomInstall, CI_VERSION, load_cifinish, InvalidCIFinishError, InstallStatus 29 | 30 | if TYPE_CHECKING: 31 | from os import PathLike 32 | from typing import Dict, List, Union 33 | 34 | frozen = getattr(sys, 'frozen', None) 35 | is_windows = sys.platform == 'win32' 36 | taskbar = None 37 | if is_windows: 38 | if frozen: 39 | # attempt to fix loading tcl/tk when running from a path with non-latin characters 40 | tkinter_path = dirname(tk.__file__) 41 | tcl_path = join(tkinter_path, 'tcl8.6') 42 | environ['TCL_LIBRARY'] = 'lib/tkinter/tcl8.6' 43 | try: 44 | import comtypes.client as cc 45 | 46 | tbl = cc.GetModule('TaskbarLib.tlb') 47 | 48 | taskbar = cc.CreateObject('{56FDF344-FD6D-11D0-958A-006097C9A090}', interface=tbl.ITaskbarList3) 49 | taskbar.HrInit() 50 | except (ModuleNotFoundError, UnicodeEncodeError, AttributeError): 51 | pass 52 | 53 | file_parent = dirname(abspath(__file__)) 54 | 55 | # automatically load boot9 if it's in the current directory 56 | b9_paths.insert(0, join(file_parent, 'boot9.bin')) 57 | b9_paths.insert(0, join(file_parent, 'boot9_prot.bin')) 58 | 59 | seeddb_paths = [join(x, 'seeddb.bin') for x in config_dirs] 60 | try: 61 | seeddb_paths.insert(0, environ['SEEDDB_PATH']) 62 | except KeyError: 63 | pass 64 | # automatically load seeddb if it's in the current directory 65 | seeddb_paths.insert(0, join(file_parent, 'seeddb.bin')) 66 | 67 | 68 | def clamp(n, smallest, largest): 69 | return max(smallest, min(n, largest)) 70 | 71 | 72 | def find_first_file(paths): 73 | for p in paths: 74 | if isfile(p): 75 | return p 76 | 77 | 78 | # find boot9, seeddb, and movable.sed to auto-select in the gui 79 | default_b9_path = find_first_file(b9_paths) 80 | default_seeddb_path = find_first_file(seeddb_paths) 81 | default_movable_sed_path = find_first_file([join(file_parent, 'movable.sed')]) 82 | 83 | if default_seeddb_path: 84 | load_seeddb(default_seeddb_path) 85 | 86 | statuses = { 87 | InstallStatus.Waiting: 'Waiting', 88 | InstallStatus.Starting: 'Starting', 89 | InstallStatus.Writing: 'Writing', 90 | InstallStatus.Finishing: 'Finishing', 91 | InstallStatus.Done: 'Done', 92 | InstallStatus.Failed: 'Failed', 93 | } 94 | 95 | 96 | class ConsoleFrame(ttk.Frame): 97 | def __init__(self, parent: tk.BaseWidget = None, starting_lines: 'List[str]' = None): 98 | super().__init__(parent) 99 | self.parent = parent 100 | 101 | self.rowconfigure(0, weight=1) 102 | self.columnconfigure(0, weight=1) 103 | 104 | scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) 105 | scrollbar.grid(row=0, column=1, sticky=tk.NSEW) 106 | 107 | self.text = tk.Text(self, highlightthickness=0, wrap='word', yscrollcommand=scrollbar.set) 108 | self.text.grid(row=0, column=0, sticky=tk.NSEW) 109 | 110 | scrollbar.config(command=self.text.yview) 111 | 112 | if starting_lines: 113 | for l in starting_lines: 114 | self.text.insert(tk.END, l + '\n') 115 | 116 | self.text.see(tk.END) 117 | self.text.configure(state=tk.DISABLED) 118 | 119 | def log(self, *message, end='\n', sep=' '): 120 | self.text.configure(state=tk.NORMAL) 121 | self.text.insert(tk.END, sep.join(message) + end) 122 | self.text.see(tk.END) 123 | self.text.configure(state=tk.DISABLED) 124 | 125 | 126 | def simple_listbox_frame(parent, title: 'str', items: 'List[str]'): 127 | frame = ttk.LabelFrame(parent, text=title) 128 | frame.rowconfigure(0, weight=1) 129 | frame.columnconfigure(0, weight=1) 130 | 131 | scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) 132 | scrollbar.grid(row=0, column=1, sticky=tk.NSEW) 133 | 134 | box = tk.Listbox(frame, highlightthickness=0, yscrollcommand=scrollbar.set, selectmode=tk.EXTENDED) 135 | box.grid(row=0, column=0, sticky=tk.NSEW) 136 | scrollbar.config(command=box.yview) 137 | 138 | box.insert(tk.END, *items) 139 | 140 | box.config(height=clamp(len(items), 3, 10)) 141 | 142 | return frame 143 | 144 | 145 | class TitleReadFailResults(tk.Toplevel): 146 | def __init__(self, parent: tk.Tk = None, *, failed: 'Dict[str, str]'): 147 | super().__init__(parent) 148 | self.parent = parent 149 | 150 | self.wm_withdraw() 151 | self.wm_transient(self.parent) 152 | self.grab_set() 153 | self.wm_title('Failed to add titles') 154 | 155 | self.rowconfigure(0, weight=1) 156 | self.columnconfigure(0, weight=1) 157 | 158 | outer_container = ttk.Frame(self) 159 | outer_container.grid(sticky=tk.NSEW) 160 | outer_container.rowconfigure(0, weight=0) 161 | outer_container.rowconfigure(1, weight=1) 162 | outer_container.columnconfigure(0, weight=1) 163 | 164 | message_label = ttk.Label(outer_container, text="Some titles couldn't be added.") 165 | message_label.grid(row=0, column=0, sticky=tk.NSEW, padx=10, pady=10) 166 | 167 | treeview_frame = ttk.Frame(outer_container) 168 | treeview_frame.grid(row=1, column=0, sticky=tk.NSEW) 169 | treeview_frame.rowconfigure(0, weight=1) 170 | treeview_frame.columnconfigure(0, weight=1) 171 | 172 | treeview_scrollbar = ttk.Scrollbar(treeview_frame, orient=tk.VERTICAL) 173 | treeview_scrollbar.grid(row=0, column=1, sticky=tk.NSEW) 174 | 175 | treeview = ttk.Treeview(treeview_frame, yscrollcommand=treeview_scrollbar.set) 176 | treeview.grid(row=0, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10)) 177 | treeview.configure(columns=('filepath', 'reason'), show='headings') 178 | 179 | treeview.column('filepath', width=200, anchor=tk.W) 180 | treeview.heading('filepath', text='File path') 181 | treeview.column('reason', width=400, anchor=tk.W) 182 | treeview.heading('reason', text='Reason') 183 | 184 | treeview_scrollbar.configure(command=treeview.yview) 185 | 186 | for path, reason in failed.items(): 187 | treeview.insert('', tk.END, text=path, iid=path, values=(basename(path), reason)) 188 | 189 | ok_frame = ttk.Frame(outer_container) 190 | ok_frame.grid(row=2, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10)) 191 | ok_frame.rowconfigure(0, weight=1) 192 | ok_frame.columnconfigure(0, weight=1) 193 | 194 | ok_button = ttk.Button(ok_frame, text='OK', command=self.destroy) 195 | ok_button.grid(row=0, column=0) 196 | 197 | self.wm_deiconify() 198 | 199 | 200 | class InstallResults(tk.Toplevel): 201 | def __init__(self, parent: tk.Tk = None, *, install_state: 'Dict[str, List[str]]', copied_3dsx: bool, 202 | application_count: int): 203 | super().__init__(parent) 204 | self.parent = parent 205 | 206 | self.wm_withdraw() 207 | self.wm_transient(self.parent) 208 | self.grab_set() 209 | self.wm_title('Install results') 210 | 211 | self.rowconfigure(0, weight=1) 212 | self.columnconfigure(0, weight=1) 213 | 214 | outer_container = ttk.Frame(self) 215 | outer_container.grid(sticky=tk.NSEW) 216 | outer_container.rowconfigure(0, weight=0) 217 | outer_container.columnconfigure(0, weight=1) 218 | 219 | if install_state['failed'] and install_state['installed']: 220 | # some failed and some worked 221 | message = ('Some titles were installed, some failed. Please check the output for more details.\n' 222 | 'The ones that were installed can be finished with custom-install-finalize.') 223 | elif install_state['failed'] and not install_state['installed']: 224 | # all failed 225 | message = 'All titles failed to install. Please check the output for more details.' 226 | elif install_state['installed'] and not install_state['failed']: 227 | # all worked 228 | message = 'All titles were installed.' 229 | else: 230 | message = 'Nothing was installed.' 231 | 232 | if install_state['installed'] and copied_3dsx: 233 | message += '\n\ncustom-install-finalize has been copied to the SD card.' 234 | 235 | if application_count >= 300: 236 | message += (f'\n\nWarning: {application_count} installed applications were detected.\n' 237 | f'The HOME Menu will only show 300 icons.\n' 238 | f'Some applications (not updates or DLC) will need to be deleted.') 239 | 240 | message_label = ttk.Label(outer_container, text=message) 241 | message_label.grid(row=0, column=0, sticky=tk.NSEW, padx=10, pady=10) 242 | 243 | if install_state['installed']: 244 | outer_container.rowconfigure(1, weight=1) 245 | frame = simple_listbox_frame(outer_container, 'Installed', install_state['installed']) 246 | frame.grid(row=1, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10)) 247 | 248 | if install_state['failed']: 249 | outer_container.rowconfigure(2, weight=1) 250 | frame = simple_listbox_frame(outer_container, 'Failed', install_state['failed']) 251 | frame.grid(row=2, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10)) 252 | 253 | ok_frame = ttk.Frame(outer_container) 254 | ok_frame.grid(row=3, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10)) 255 | ok_frame.rowconfigure(0, weight=1) 256 | ok_frame.columnconfigure(0, weight=1) 257 | 258 | ok_button = ttk.Button(ok_frame, text='OK', command=self.destroy) 259 | ok_button.grid(row=0, column=0) 260 | 261 | self.wm_deiconify() 262 | 263 | 264 | class CustomInstallGUI(ttk.Frame): 265 | console = None 266 | b9_loaded = False 267 | 268 | def __init__(self, parent: tk.Tk = None): 269 | super().__init__(parent) 270 | self.parent = parent 271 | 272 | # readers to give to CustomInstall at the install 273 | self.readers = {} 274 | 275 | self.lock = Lock() 276 | 277 | self.log_messages = [] 278 | 279 | self.hwnd = None # will be set later 280 | 281 | self.rowconfigure(2, weight=1) 282 | self.columnconfigure(0, weight=1) 283 | 284 | if taskbar: 285 | # this is so progress can be shown in the taskbar 286 | def setup_tab(): 287 | self.hwnd = int(parent.wm_frame(), 16) 288 | taskbar.ActivateTab(self.hwnd) 289 | 290 | self.after(100, setup_tab) 291 | 292 | # ---------------------------------------------------------------- # 293 | # create file pickers for base files 294 | file_pickers = ttk.Frame(self) 295 | file_pickers.grid(row=0, column=0, sticky=tk.EW) 296 | file_pickers.columnconfigure(1, weight=1) 297 | 298 | self.file_picker_textboxes = {} 299 | 300 | def sd_callback(): 301 | f = fd.askdirectory(parent=parent, title='Select SD root (the directory or drive that contains ' 302 | '"Nintendo 3DS")', initialdir=file_parent, mustexist=True) 303 | if f: 304 | cifinish_path = join(f, 'cifinish.bin') 305 | try: 306 | load_cifinish(cifinish_path) 307 | except InvalidCIFinishError: 308 | self.show_error(f'{cifinish_path} was corrupt!\n\n' 309 | f'This could mean an issue with the SD card or the filesystem. Please check it for errors.\n' 310 | f'It is also possible, though less likely, to be an issue with custom-install.\n\n' 311 | f'Stopping now to prevent possible issues. If you want to try again, delete cifinish.bin from the SD card and re-run custom-install.') 312 | return 313 | 314 | sd_selected.delete('1.0', tk.END) 315 | sd_selected.insert(tk.END, f) 316 | 317 | for filename in ['boot9.bin', 'seeddb.bin', 'movable.sed']: 318 | path = auto_input_filename(self, f, filename) 319 | if filename == 'boot9.bin': 320 | self.check_b9_loaded() 321 | self.enable_buttons() 322 | if filename == 'seeddb.bin': 323 | load_seeddb(path) 324 | 325 | 326 | sd_type_label = ttk.Label(file_pickers, text='SD root') 327 | sd_type_label.grid(row=0, column=0) 328 | 329 | sd_selected = tk.Text(file_pickers, wrap='none', height=1) 330 | sd_selected.grid(row=0, column=1, sticky=tk.EW) 331 | 332 | sd_button = ttk.Button(file_pickers, text='...', command=sd_callback) 333 | sd_button.grid(row=0, column=2) 334 | 335 | self.file_picker_textboxes['sd'] = sd_selected 336 | 337 | def auto_input_filename(self, f, filename): 338 | sd_msed_path = find_first_file([join(f, 'gm9', 'out', filename), join(f, filename)]) 339 | if sd_msed_path: 340 | self.log('Found ' + filename + ' on SD card at ' + sd_msed_path) 341 | if filename.endswith('bin'): 342 | filename = filename.split('.')[0] 343 | box = self.file_picker_textboxes[filename] 344 | box.delete('1.0', tk.END) 345 | box.insert(tk.END, sd_msed_path) 346 | return sd_msed_path 347 | # This feels so wrong. 348 | def create_required_file_picker(type_name, types, default, row, callback=lambda filename: None): 349 | def internal_callback(): 350 | f = fd.askopenfilename(parent=parent, title='Select ' + type_name, filetypes=types, 351 | initialdir=file_parent) 352 | if f: 353 | selected.delete('1.0', tk.END) 354 | selected.insert(tk.END, f) 355 | callback(f) 356 | 357 | type_label = ttk.Label(file_pickers, text=type_name) 358 | type_label.grid(row=row, column=0) 359 | 360 | selected = tk.Text(file_pickers, wrap='none', height=1) 361 | selected.grid(row=row, column=1, sticky=tk.EW) 362 | if default: 363 | selected.insert(tk.END, default) 364 | 365 | button = ttk.Button(file_pickers, text='...', command=internal_callback) 366 | button.grid(row=row, column=2) 367 | 368 | self.file_picker_textboxes[type_name] = selected 369 | 370 | def b9_callback(path: 'Union[PathLike, bytes, str]'): 371 | self.check_b9_loaded() 372 | self.enable_buttons() 373 | 374 | def seeddb_callback(path: 'Union[PathLike, bytes, str]'): 375 | load_seeddb(path) 376 | 377 | create_required_file_picker('boot9', [('boot9 file', '*.bin')], default_b9_path, 1, b9_callback) 378 | create_required_file_picker('seeddb', [('seeddb file', '*.bin')], default_seeddb_path, 2, seeddb_callback) 379 | create_required_file_picker('movable.sed', [('movable.sed file', '*.sed')], default_movable_sed_path, 3) 380 | 381 | # ---------------------------------------------------------------- # 382 | # create buttons to add cias 383 | titlelist_buttons = ttk.Frame(self) 384 | titlelist_buttons.grid(row=1, column=0) 385 | 386 | def add_cias_callback(): 387 | files = fd.askopenfilenames(parent=parent, title='Select CIA files', filetypes=[('CIA files', '*.cia')], 388 | initialdir=file_parent) 389 | results = {} 390 | for f in files: 391 | success, reason = self.add_cia(f) 392 | if not success: 393 | results[f] = reason 394 | 395 | if results: 396 | title_read_fail_window = TitleReadFailResults(self.parent, failed=results) 397 | title_read_fail_window.focus() 398 | self.sort_treeview() 399 | 400 | add_cias = ttk.Button(titlelist_buttons, text='Add CIAs', command=add_cias_callback) 401 | add_cias.grid(row=0, column=0) 402 | 403 | def add_cdn_callback(): 404 | d = fd.askdirectory(parent=parent, title='Select folder containing title contents in CDN format', 405 | initialdir=file_parent) 406 | if d: 407 | if isfile(join(d, 'tmd')): 408 | success, reason = self.add_cia(d) 409 | if not success: 410 | self.show_error(f"Couldn't add {basename(d)}: {reason}") 411 | else: 412 | self.sort_treeview() 413 | else: 414 | self.show_error('tmd file not found in the CDN directory:\n' + d) 415 | 416 | add_cdn = ttk.Button(titlelist_buttons, text='Add CDN title folder', command=add_cdn_callback) 417 | add_cdn.grid(row=0, column=1) 418 | 419 | def add_dirs_callback(): 420 | d = fd.askdirectory(parent=parent, title='Select folder containing CIA files', initialdir=file_parent) 421 | if d: 422 | results = {} 423 | for f in scandir(d): 424 | if f.name.lower().endswith('.cia'): 425 | success, reason = self.add_cia(f.path) 426 | if not success: 427 | results[f] = reason 428 | 429 | if results: 430 | title_read_fail_window = TitleReadFailResults(self.parent, failed=results) 431 | title_read_fail_window.focus() 432 | self.sort_treeview() 433 | 434 | add_dirs = ttk.Button(titlelist_buttons, text='Add folder', command=add_dirs_callback) 435 | add_dirs.grid(row=0, column=2) 436 | 437 | def remove_selected_callback(): 438 | for entry in self.treeview.selection(): 439 | self.remove_cia(entry) 440 | 441 | remove_selected = ttk.Button(titlelist_buttons, text='Remove selected', command=remove_selected_callback) 442 | remove_selected.grid(row=0, column=3) 443 | 444 | # ---------------------------------------------------------------- # 445 | # create treeview 446 | treeview_frame = ttk.Frame(self) 447 | treeview_frame.grid(row=2, column=0, sticky=tk.NSEW) 448 | treeview_frame.rowconfigure(0, weight=1) 449 | treeview_frame.columnconfigure(0, weight=1) 450 | 451 | treeview_scrollbar = ttk.Scrollbar(treeview_frame, orient=tk.VERTICAL) 452 | treeview_scrollbar.grid(row=0, column=1, sticky=tk.NSEW) 453 | 454 | self.treeview = ttk.Treeview(treeview_frame, yscrollcommand=treeview_scrollbar.set) 455 | self.treeview.grid(row=0, column=0, sticky=tk.NSEW) 456 | self.treeview.configure(columns=('filepath', 'titleid', 'titlename', 'status'), show='headings') 457 | 458 | self.treeview.column('filepath', width=200, anchor=tk.W) 459 | self.treeview.heading('filepath', text='File path') 460 | self.treeview.column('titleid', width=70, anchor=tk.W) 461 | self.treeview.heading('titleid', text='Title ID') 462 | self.treeview.column('titlename', width=150, anchor=tk.W) 463 | self.treeview.heading('titlename', text='Title name') 464 | self.treeview.column('status', width=20, anchor=tk.W) 465 | self.treeview.heading('status', text='Status') 466 | 467 | treeview_scrollbar.configure(command=self.treeview.yview) 468 | 469 | # ---------------------------------------------------------------- # 470 | # create progressbar 471 | 472 | self.progressbar = ttk.Progressbar(self, orient=tk.HORIZONTAL, mode='determinate') 473 | self.progressbar.grid(row=3, column=0, sticky=tk.NSEW) 474 | 475 | # ---------------------------------------------------------------- # 476 | # create start and console buttons 477 | 478 | control_frame = ttk.Frame(self) 479 | control_frame.grid(row=4, column=0) 480 | 481 | self.skip_contents_var = tk.IntVar() 482 | skip_contents_checkbox = ttk.Checkbutton(control_frame, text='Skip contents (only add to title database)', 483 | variable=self.skip_contents_var) 484 | skip_contents_checkbox.grid(row=0, column=0) 485 | 486 | self.overwrite_saves_var = tk.IntVar() 487 | overwrite_saves_checkbox = ttk.Checkbutton(control_frame, text='Overwrite existing saves', 488 | variable=self.overwrite_saves_var) 489 | overwrite_saves_checkbox.grid(row=0, column=1) 490 | 491 | show_console = ttk.Button(control_frame, text='Show console', command=self.open_console) 492 | show_console.grid(row=0, column=2) 493 | 494 | start = ttk.Button(control_frame, text='Start install', command=self.start_install) 495 | start.grid(row=0, column=3) 496 | 497 | self.status_label = ttk.Label(self, text='Waiting...') 498 | self.status_label.grid(row=5, column=0, sticky=tk.NSEW) 499 | 500 | self.log(f'custom-install {CI_VERSION} - https://github.com/ihaveamac/custom-install', status=False) 501 | 502 | if is_windows and not taskbar: 503 | self.log('Note: Could not load taskbar lib.') 504 | self.log('Note: Progress will not be shown in the Windows taskbar.') 505 | 506 | self.log('Ready.') 507 | 508 | self.require_boot9 = (add_cias, add_cdn, add_dirs, remove_selected, start) 509 | 510 | self.disable_buttons() 511 | self.check_b9_loaded() 512 | self.enable_buttons() 513 | if not self.b9_loaded: 514 | self.log('Note: boot9 was not auto-detected. Please choose it before adding any titles.') 515 | 516 | def sort_treeview(self): 517 | l = [(self.treeview.set(k, 'titlename'), k) for k in self.treeview.get_children()] 518 | # sort by title name 519 | l.sort(key=lambda x: x[0].lower()) 520 | 521 | for idx, pair in enumerate(l): 522 | self.treeview.move(pair[1], '', idx) 523 | 524 | def check_b9_loaded(self): 525 | if not self.b9_loaded: 526 | boot9 = self.file_picker_textboxes['boot9'].get('1.0', tk.END).strip() 527 | try: 528 | tmp_crypto = CryptoEngine(boot9=boot9) 529 | self.b9_loaded = tmp_crypto.b9_keys_set 530 | except: 531 | return False 532 | return self.b9_loaded 533 | 534 | def update_status(self, path: 'Union[PathLike, bytes, str]', status: InstallStatus): 535 | self.treeview.set(path, 'status', statuses[status]) 536 | 537 | def add_cia(self, path): 538 | if not self.check_b9_loaded(): 539 | # this shouldn't happen 540 | return False, 'Please choose boot9 first' 541 | path = abspath(path) 542 | if path in self.readers: 543 | return False, 'File already in list' 544 | try: 545 | reader = CustomInstall.get_reader(path) 546 | except (CIAError, CDNError, TitleMetadataError): 547 | return False, 'Failed to read as a CIA or CDN title, probably corrupt' 548 | except MissingSeedError: 549 | return False, 'Latest seeddb.bin is required, check the README for details' 550 | except Exception as e: 551 | return False, f'Exception occurred: {type(e).__name__}: {e}' 552 | 553 | if reader.tmd.title_id.startswith('00048'): 554 | return False, 'DSiWare is not supported' 555 | try: 556 | title_name = reader.contents[0].exefs.icon.get_app_title().short_desc 557 | except: 558 | title_name = '(No title)' 559 | self.treeview.insert('', tk.END, text=path, iid=path, 560 | values=(path, reader.tmd.title_id, title_name, statuses[InstallStatus.Waiting])) 561 | self.readers[path] = reader 562 | return True, '' 563 | 564 | def remove_cia(self, path): 565 | self.treeview.delete(path) 566 | del self.readers[path] 567 | 568 | def open_console(self): 569 | if self.console: 570 | self.console.parent.lift() 571 | self.console.focus() 572 | else: 573 | console_window = tk.Toplevel() 574 | console_window.title('custom-install Console') 575 | 576 | self.console = ConsoleFrame(console_window, self.log_messages) 577 | self.console.pack(fill=tk.BOTH, expand=True) 578 | 579 | def close(): 580 | with self.lock: 581 | try: 582 | console_window.destroy() 583 | except: 584 | pass 585 | self.console = None 586 | 587 | console_window.focus() 588 | 589 | console_window.protocol('WM_DELETE_WINDOW', close) 590 | 591 | def log(self, line, status=True): 592 | with self.lock: 593 | log_msg = f"{strftime('%H:%M:%S')} - {line}" 594 | self.log_messages.append(log_msg) 595 | if self.console: 596 | self.console.log(log_msg) 597 | 598 | if status: 599 | self.status_label.config(text=line) 600 | 601 | def show_error(self, message): 602 | mb.showerror('Error', message, parent=self.parent) 603 | 604 | def ask_warning(self, message): 605 | return mb.askokcancel('Warning', message, parent=self.parent) 606 | 607 | def show_info(self, message): 608 | mb.showinfo('Info', message, parent=self.parent) 609 | 610 | def disable_buttons(self): 611 | for b in self.require_boot9: 612 | b.config(state=tk.DISABLED) 613 | for b in self.file_picker_textboxes.values(): 614 | b.config(state=tk.DISABLED) 615 | 616 | def enable_buttons(self): 617 | if self.b9_loaded: 618 | for b in self.require_boot9: 619 | b.config(state=tk.NORMAL) 620 | for b in self.file_picker_textboxes.values(): 621 | b.config(state=tk.NORMAL) 622 | 623 | def start_install(self): 624 | sd_root = self.file_picker_textboxes['sd'].get('1.0', tk.END).strip() 625 | seeddb = self.file_picker_textboxes['seeddb'].get('1.0', tk.END).strip() 626 | movable_sed = self.file_picker_textboxes['movable.sed'].get('1.0', tk.END).strip() 627 | 628 | if not sd_root: 629 | self.show_error('SD root is not specified.') 630 | return 631 | if not movable_sed: 632 | self.show_error('movable.sed is not specified.') 633 | return 634 | 635 | if not seeddb: 636 | if not self.ask_warning('seeddb was not specified. Titles that require it will fail to install.\n' 637 | 'Continue?'): 638 | return 639 | 640 | if not len(self.readers): 641 | self.show_error('There are no titles added to install.') 642 | return 643 | 644 | for path in self.readers.keys(): 645 | self.update_status(path, InstallStatus.Waiting) 646 | self.disable_buttons() 647 | 648 | if taskbar: 649 | taskbar.SetProgressState(self.hwnd, tbl.TBPF_NORMAL) 650 | 651 | installer = CustomInstall(movable=movable_sed, 652 | sd=sd_root, 653 | skip_contents=self.skip_contents_var.get() == 1, 654 | overwrite_saves=self.overwrite_saves_var.get() == 1) 655 | 656 | if not installer.check_for_id0(): 657 | self.show_error(f'id0 {installer.crypto.id0.hex()} was not found inside "Nintendo 3DS" on the SD card.\n' 658 | f'\n' 659 | f'Before using custom-install, you should use this SD card on the appropriate console.\n' 660 | f'\n' 661 | f'Otherwise, make sure the correct movable.sed is being used.') 662 | return 663 | 664 | self.log('Starting install...') 665 | 666 | # use the treeview which has been sorted alphabetically 667 | readers_final = [] 668 | for k in self.treeview.get_children(): 669 | filepath = self.treeview.set(k, 'filepath') 670 | readers_final.append((self.readers[filepath], filepath)) 671 | 672 | installer.readers = readers_final 673 | 674 | finished_percent = 0 675 | max_percentage = 100 * len(self.readers) 676 | self.progressbar.config(maximum=max_percentage) 677 | 678 | def ci_on_log_msg(message, *args, **kwargs): 679 | # ignoring end 680 | self.log(message) 681 | 682 | def ci_update_percentage(total_percent, total_read, size): 683 | self.progressbar.config(value=total_percent + finished_percent) 684 | if taskbar: 685 | taskbar.SetProgressValue(self.hwnd, int(total_percent + finished_percent), max_percentage) 686 | 687 | def ci_on_error(exc): 688 | if taskbar: 689 | taskbar.SetProgressState(self.hwnd, tbl.TBPF_ERROR) 690 | for line in format_exception(*exc): 691 | for line2 in line.split('\n')[:-1]: 692 | installer.log(line2) 693 | self.show_error('An error occurred during installation.') 694 | self.open_console() 695 | 696 | def ci_on_cia_start(idx): 697 | nonlocal finished_percent 698 | finished_percent = idx * 100 699 | if taskbar: 700 | taskbar.SetProgressValue(self.hwnd, finished_percent, max_percentage) 701 | 702 | installer.event.on_log_msg += ci_on_log_msg 703 | installer.event.update_percentage += ci_update_percentage 704 | installer.event.on_error += ci_on_error 705 | installer.event.on_cia_start += ci_on_cia_start 706 | installer.event.update_status += self.update_status 707 | 708 | if self.skip_contents_var.get() != 1: 709 | total_size, free_space = installer.check_size() 710 | if total_size > free_space: 711 | self.show_error(f'Not enough free space.\n' 712 | f'Combined title install size: {total_size / (1024 * 1024):0.2f} MiB\n' 713 | f'Free space: {free_space / (1024 * 1024):0.2f} MiB') 714 | self.enable_buttons() 715 | return 716 | 717 | def install(): 718 | try: 719 | result, copied_3dsx, application_count = installer.start() 720 | if result: 721 | result_window = InstallResults(self.parent, 722 | install_state=result, 723 | copied_3dsx=copied_3dsx, 724 | application_count=application_count) 725 | result_window.focus() 726 | elif result is None: 727 | self.show_error("An error occurred when trying to run save3ds_fuse.\n" 728 | "Either title.db doesn't exist, or save3ds_fuse couldn't be run.") 729 | self.open_console() 730 | except: 731 | installer.event.on_error(sys.exc_info()) 732 | finally: 733 | self.enable_buttons() 734 | 735 | Thread(target=install).start() 736 | 737 | 738 | window = tk.Tk() 739 | window.title(f'custom-install {CI_VERSION}') 740 | frame = CustomInstallGUI(window) 741 | frame.pack(fill=tk.BOTH, expand=True) 742 | window.mainloop() 743 | -------------------------------------------------------------------------------- /custominstall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is a part of custom-install.py. 4 | # 5 | # custom-install is copyright (c) 2019-2020 Ian Burgwin 6 | # This file is licensed under The MIT License (MIT). 7 | # You can find the full license text in LICENSE.md in the root of this project. 8 | 9 | from argparse import ArgumentParser 10 | from enum import Enum 11 | from glob import glob 12 | import gzip 13 | from os import makedirs, rename, scandir 14 | from os.path import dirname, join, isdir, isfile 15 | from random import randint 16 | from hashlib import sha256 17 | from pprint import pformat 18 | from shutil import copyfile, copy2, rmtree 19 | import sys 20 | from sys import platform, executable 21 | from tempfile import TemporaryDirectory 22 | from traceback import format_exception 23 | from typing import BinaryIO, TYPE_CHECKING 24 | import subprocess 25 | 26 | if TYPE_CHECKING: 27 | from os import PathLike 28 | from typing import List, Union, Tuple 29 | 30 | from events import Events 31 | 32 | from pyctr.crypto import CryptoEngine, Keyslot, load_seeddb, get_seed 33 | from pyctr.type.cdn import CDNReader, CDNError 34 | from pyctr.type.cia import CIAReader, CIAError 35 | from pyctr.type.ncch import NCCHSection 36 | from pyctr.type.tmd import TitleMetadataError 37 | from pyctr.util import roundup 38 | 39 | if platform == 'msys': 40 | platform = 'win32' 41 | 42 | is_windows = platform == 'win32' 43 | 44 | if is_windows: 45 | from ctypes import c_wchar_p, pointer, c_ulonglong, windll 46 | else: 47 | from os import statvfs 48 | 49 | CI_VERSION = '2.1' 50 | 51 | # used to run the save3ds_fuse binary next to the script 52 | frozen = getattr(sys, 'frozen', False) 53 | script_dir: str 54 | if frozen: 55 | script_dir = dirname(executable) 56 | else: 57 | script_dir = dirname(__file__) 58 | 59 | # missing contents are replaced with 0xFFFFFFFF in the cmd file 60 | CMD_MISSING = b'\xff\xff\xff\xff' 61 | 62 | # the size of each file and directory in a title's contents are rounded up to this 63 | TITLE_ALIGN_SIZE = 0x8000 64 | 65 | # size to read at a time when copying files 66 | READ_SIZE = 0x200000 67 | 68 | # version for cifinish.bin 69 | CIFINISH_VERSION = 3 70 | 71 | 72 | # Placeholder for SDPathErrors 73 | class SDPathError(Exception): 74 | pass 75 | 76 | 77 | class InvalidCIFinishError(Exception): 78 | pass 79 | 80 | 81 | class InstallStatus(Enum): 82 | Waiting = 0 83 | Starting = 1 84 | Writing = 2 85 | Finishing = 3 86 | Done = 4 87 | Failed = 5 88 | 89 | 90 | def get_free_space(path: 'Union[PathLike, bytes, str]'): 91 | if is_windows: 92 | lpSectorsPerCluster = c_ulonglong(0) 93 | lpBytesPerSector = c_ulonglong(0) 94 | lpNumberOfFreeClusters = c_ulonglong(0) 95 | lpTotalNumberOfClusters = c_ulonglong(0) 96 | ret = windll.kernel32.GetDiskFreeSpaceW(c_wchar_p(path), pointer(lpSectorsPerCluster), 97 | pointer(lpBytesPerSector), 98 | pointer(lpNumberOfFreeClusters), 99 | pointer(lpTotalNumberOfClusters)) 100 | if not ret: 101 | raise WindowsError 102 | free_blocks = lpNumberOfFreeClusters.value * lpSectorsPerCluster.value 103 | free_bytes = free_blocks * lpBytesPerSector.value 104 | else: 105 | stv = statvfs(path) 106 | free_bytes = stv.f_bavail * stv.f_frsize 107 | return free_bytes 108 | 109 | 110 | def load_cifinish(path: 'Union[PathLike, bytes, str]'): 111 | try: 112 | with open(path, 'rb') as f: 113 | header = f.read(0x10) 114 | if header[0:8] != b'CIFINISH': 115 | raise InvalidCIFinishError('CIFINISH magic not found') 116 | version = int.from_bytes(header[0x8:0xC], 'little') 117 | count = int.from_bytes(header[0xC:0x10], 'little') 118 | data = {} 119 | for _ in range(count): 120 | if version == 1: 121 | # ignoring the titlekey and common key index, since it's not useful in this scenario 122 | raw_entry = f.read(0x30) 123 | if len(raw_entry) != 0x30: 124 | raise InvalidCIFinishError(f'title entry is not 0x30 (version {version})') 125 | 126 | title_magic = raw_entry[0xA:0x10] 127 | title_id = int.from_bytes(raw_entry[0:8], 'little') 128 | has_seed = raw_entry[0x9] 129 | seed = raw_entry[0x20:0x30] 130 | 131 | elif version == 2: 132 | # this is assuming the "wrong" version created by an earlier version of this script 133 | # there wasn't a version of custom-install-finalize that really accepted this version 134 | raw_entry = f.read(0x20) 135 | if len(raw_entry) != 0x20: 136 | raise InvalidCIFinishError(f'title entry is not 0x20 (version {version})') 137 | 138 | title_magic = raw_entry[0:6] 139 | title_id = int.from_bytes(raw_entry[0x6:0xE], 'little') 140 | has_seed = raw_entry[0xE] 141 | seed = raw_entry[0x10:0x20] 142 | 143 | elif version == 3: 144 | raw_entry = f.read(0x20) 145 | if len(raw_entry) != 0x20: 146 | raise InvalidCIFinishError(f'title entry is not 0x20 (version {version})') 147 | 148 | title_magic = raw_entry[0:6] 149 | title_id = int.from_bytes(raw_entry[0x8:0x10], 'little') 150 | has_seed = raw_entry[0x6] 151 | seed = raw_entry[0x10:0x20] 152 | 153 | else: 154 | raise InvalidCIFinishError(f'unknown version {version}') 155 | 156 | if title_magic == b'TITLE\0': 157 | data[title_id] = {'seed': seed if has_seed else None} 158 | 159 | return data 160 | except FileNotFoundError: 161 | # allow the caller to easily create a new database in the same place where an existing one would be updated 162 | return {} 163 | 164 | 165 | def save_cifinish(path: 'Union[PathLike, bytes, str]', data: dict): 166 | with open(path, 'wb') as out: 167 | entries = sorted(data.items()) 168 | 169 | out.write(b'CIFINISH') 170 | out.write(CIFINISH_VERSION.to_bytes(4, 'little')) 171 | out.write(len(entries).to_bytes(4, 'little')) 172 | 173 | for tid, data in entries: 174 | finalize_entry_data = [ 175 | # magic 176 | b'TITLE\0', 177 | # has seed 178 | bool(data['seed']).to_bytes(1, 'little'), 179 | # padding 180 | b'\0', 181 | # title id 182 | tid.to_bytes(8, 'little'), 183 | # seed, if needed 184 | (data['seed'] if data['seed'] else (b'\0' * 0x10)) 185 | ] 186 | 187 | out.write(b''.join(finalize_entry_data)) 188 | 189 | 190 | def get_install_size(title: 'Union[CIAReader, CDNReader]'): 191 | sizes = [1] * 5 192 | 193 | if title.tmd.save_size: 194 | # one for the data directory, one for the 00000001.sav file 195 | sizes.extend((1, title.tmd.save_size)) 196 | 197 | for record in title.content_info: 198 | sizes.append(record.size) 199 | 200 | # this calculates the size to put in the Title Info Entry 201 | title_size = sum(roundup(x, TITLE_ALIGN_SIZE) for x in sizes) 202 | 203 | return title_size 204 | 205 | 206 | class CustomInstall: 207 | def __init__(self, *, movable, sd, cifinish_out=None, overwrite_saves=False, skip_contents=False, 208 | boot9=None, seeddb=None): 209 | self.event = Events() 210 | self.log_lines = [] # Stores all info messages for user to view 211 | 212 | self.crypto = CryptoEngine(boot9=boot9) 213 | self.crypto.setup_sd_key_from_file(movable) 214 | self.seeddb = seeddb 215 | self.readers: 'List[Tuple[Union[CDNReader, CIAReader], Union[PathLike, bytes, str]]]' = [] 216 | self.sd = sd 217 | self.skip_contents = skip_contents 218 | self.overwrite_saves = overwrite_saves 219 | self.cifinish_out = cifinish_out 220 | self.movable = movable 221 | 222 | def copy_with_progress(self, src: BinaryIO, dst: BinaryIO, size: int, path: str, fire_event: bool = True): 223 | left = size 224 | cipher = self.crypto.create_ctr_cipher(Keyslot.SD, self.crypto.sd_path_to_iv(path)) 225 | hasher = sha256() 226 | while left > 0: 227 | to_read = min(READ_SIZE, left) 228 | data = src.read(READ_SIZE) 229 | hasher.update(data) 230 | dst.write(cipher.encrypt(data)) 231 | left -= to_read 232 | total_read = size - left 233 | if fire_event: 234 | self.event.update_percentage((total_read / size) * 100, total_read / 1048576, size / 1048576) 235 | 236 | return hasher.digest() 237 | 238 | @staticmethod 239 | def get_reader(path: 'Union[PathLike, bytes, str]'): 240 | if isdir(path): 241 | # try the default tmd file 242 | reader = CDNReader(join(path, 'tmd')) 243 | else: 244 | try: 245 | reader = CIAReader(path) 246 | except CIAError: 247 | # if there was an error with parsing the CIA header, 248 | # the file would be tried in CDNReader next (assuming it's a tmd) 249 | # any other error should be propagated to the caller 250 | reader = CDNReader(path) 251 | return reader 252 | 253 | def prepare_titles(self, paths: 'List[PathLike]'): 254 | if self.seeddb: 255 | load_seeddb(self.seeddb) 256 | 257 | readers = [] 258 | for path in paths: 259 | self.log(f'Reading {path}') 260 | try: 261 | reader = self.get_reader(path) 262 | except (CIAError, CDNError, TitleMetadataError): 263 | self.log(f"Couldn't read {path}, likely corrupt or not a CIA or CDN title") 264 | continue 265 | if reader.tmd.title_id.startswith('00048'): # DSiWare 266 | self.log(f'Skipping {reader.tmd.title_id} - DSiWare is not supported') 267 | continue 268 | readers.append((reader, path)) 269 | self.readers = readers 270 | 271 | def check_size(self): 272 | total_size = 0 273 | for r, _ in self.readers: 274 | total_size += get_install_size(r) 275 | 276 | free_space = get_free_space(self.sd) 277 | return total_size, free_space 278 | 279 | def check_for_id0(self): 280 | sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex()) 281 | return isdir(sd_path) 282 | 283 | def start(self): 284 | if frozen: 285 | save3ds_fuse_path = join(script_dir, 'bin', 'save3ds_fuse') 286 | else: 287 | save3ds_fuse_path = join(script_dir, 'bin', platform, 'save3ds_fuse') 288 | if is_windows: 289 | save3ds_fuse_path += '.exe' 290 | if not isfile(save3ds_fuse_path): 291 | self.log("Couldn't find " + save3ds_fuse_path, 2) 292 | return None, False, 0 293 | 294 | crypto = self.crypto 295 | # TODO: Move a lot of these into their own methods 296 | self.log("Finding path to install to...") 297 | [sd_path, id1s] = self.get_sd_path() 298 | if len(id1s) > 1: 299 | raise SDPathError(f'There are multiple id1 directories for id0 {crypto.id0.hex()}, ' 300 | f'please remove extra directories') 301 | elif len(id1s) == 0: 302 | raise SDPathError(f'Could not find a suitable id1 directory for id0 {crypto.id0.hex()}') 303 | id1 = id1s[0] 304 | sd_path = join(sd_path, id1) 305 | 306 | if self.cifinish_out: 307 | cifinish_path = self.cifinish_out 308 | else: 309 | cifinish_path = join(self.sd, 'cifinish.bin') 310 | 311 | try: 312 | cifinish_data = load_cifinish(cifinish_path) 313 | except InvalidCIFinishError as e: 314 | self.log(f'{type(e).__qualname__}: {e}') 315 | self.log(f'{cifinish_path} was corrupt!\n' 316 | f'This could mean an issue with the SD card or the filesystem. Please check it for errors.\n' 317 | f'It is also possible, though less likely, to be an issue with custom-install.\n' 318 | f'Exiting now to prevent possible issues. If you want to try again, delete cifinish.bin from the SD card and re-run custom-install.') 319 | return None, False, 0 320 | 321 | db_path = join(sd_path, 'dbs') 322 | titledb_path = join(db_path, 'title.db') 323 | importdb_path = join(db_path, 'import.db') 324 | if not isfile(titledb_path): 325 | makedirs(db_path, exist_ok=True) 326 | with gzip.open(join(script_dir, 'title.db.gz')) as f: 327 | tdb = f.read() 328 | 329 | self.log(f'Creating title.db...') 330 | with open(titledb_path, 'wb') as o: 331 | with self.crypto.create_ctr_io(Keyslot.SD, o, self.crypto.sd_path_to_iv('/dbs/title.db')) as e: 332 | e.write(tdb) 333 | 334 | cmac = crypto.create_cmac_object(Keyslot.CMACSDNAND) 335 | cmac_data = [b'CTR-9DB0', 0x2.to_bytes(4, 'little'), tdb[0x100:0x200]] 336 | cmac.update(sha256(b''.join(cmac_data)).digest()) 337 | 338 | e.seek(0) 339 | e.write(cmac.digest()) 340 | 341 | self.log(f'Creating import.db...') 342 | with open(importdb_path, 'wb') as o: 343 | with self.crypto.create_ctr_io(Keyslot.SD, o, self.crypto.sd_path_to_iv('/dbs/import.db')) as e: 344 | e.write(tdb) 345 | 346 | cmac = crypto.create_cmac_object(Keyslot.CMACSDNAND) 347 | cmac_data = [b'CTR-9DB0', 0x3.to_bytes(4, 'little'), tdb[0x100:0x200]] 348 | cmac.update(sha256(b''.join(cmac_data)).digest()) 349 | 350 | e.seek(0) 351 | e.write(cmac.digest()) 352 | 353 | del tdb 354 | 355 | with TemporaryDirectory(suffix='-custom-install') as tempdir: 356 | # set up the common arguments for the two times we call save3ds_fuse 357 | save3ds_fuse_common_args = [ 358 | save3ds_fuse_path, 359 | '-b', crypto.b9_path, 360 | '-m', self.movable, 361 | '--sd', self.sd, 362 | '--db', 'sdtitle', 363 | tempdir 364 | ] 365 | 366 | extra_kwargs = {} 367 | if is_windows: 368 | # hide console window 369 | extra_kwargs['creationflags'] = 0x08000000 # CREATE_NO_WINDOW 370 | 371 | # extract the title database to add our own entry to 372 | self.log('Extracting Title Database...') 373 | out = subprocess.run(save3ds_fuse_common_args + ['-x'], 374 | stdout=subprocess.PIPE, 375 | stderr=subprocess.STDOUT, 376 | encoding='utf-8', 377 | **extra_kwargs) 378 | if out.returncode: 379 | for l in out.stdout.split('\n'): 380 | self.log(l) 381 | self.log('Command line:') 382 | for l in pformat(out.args).split('\n'): 383 | self.log(l) 384 | return None, False, 0 385 | 386 | install_state = {'installed': [], 'failed': []} 387 | 388 | # Now loop through all provided cia files 389 | for idx, info in enumerate(self.readers): 390 | cia, path = info 391 | 392 | self.event.on_cia_start(idx) 393 | self.event.update_status(path, InstallStatus.Starting) 394 | 395 | temp_title_root = join(self.sd, f'ci-install-temp-{cia.tmd.title_id}-{randint(0, 0xFFFFFFFF):08x}') 396 | makedirs(temp_title_root, exist_ok=True) 397 | 398 | tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16]) 399 | 400 | try: 401 | display_title = f'{cia.contents[0].exefs.icon.get_app_title().short_desc} - {cia.tmd.title_id}' 402 | except: 403 | display_title = cia.tmd.title_id 404 | self.log(f'Installing {display_title}...') 405 | 406 | title_size = get_install_size(cia) 407 | 408 | # checks if this is dlc, which has some differences 409 | is_dlc = tid_parts[0] == '0004008c' 410 | 411 | # this checks if it has a manual (index 1) and is not DLC 412 | has_manual = (not is_dlc) and (1 in cia.contents) 413 | 414 | # this gets the extdata id from the extheader, stored in the storage info area 415 | try: 416 | with cia.contents[0].open_raw_section(NCCHSection.ExtendedHeader) as e: 417 | e.seek(0x200 + 0x30) 418 | extdata_id = e.read(8) 419 | except KeyError: 420 | # not an executable title 421 | extdata_id = b'\0' * 8 422 | 423 | # cmd content id, starts with 1 for non-dlc contents 424 | cmd_id = len(cia.content_info) if is_dlc else 1 425 | cmd_filename = f'{cmd_id:08x}.cmd' 426 | 427 | # this is where the final directory will be moved 428 | tidhigh_root = join(sd_path, 'title', tid_parts[0]) 429 | 430 | # get the title root where all the contents will be 431 | title_root = join(sd_path, 'title', *tid_parts) 432 | content_root = join(title_root, 'content') 433 | # generate the path used for the IV 434 | title_root_cmd = f'/title/{"/".join(tid_parts)}' 435 | content_root_cmd = title_root_cmd + '/content' 436 | 437 | temp_content_root = join(temp_title_root, 'content') 438 | 439 | if not self.skip_contents: 440 | self.event.update_status(path, InstallStatus.Writing) 441 | makedirs(join(temp_content_root, 'cmd'), exist_ok=True) 442 | if cia.tmd.save_size: 443 | makedirs(join(temp_title_root, 'data'), exist_ok=True) 444 | if is_dlc: 445 | # create the separate directories for every 256 contents 446 | for x in range(((len(cia.content_info) - 1) // 256) + 1): 447 | makedirs(join(temp_content_root, f'{x:08x}'), exist_ok=True) 448 | 449 | # maybe this will be changed in the future 450 | tmd_id = 0 451 | 452 | tmd_filename = f'{tmd_id:08x}.tmd' 453 | 454 | # write the tmd 455 | tmd_enc_path = content_root_cmd + '/' + tmd_filename 456 | self.log(f'Writing {tmd_enc_path}...') 457 | with open(join(temp_content_root, tmd_filename), 'wb') as o: 458 | with self.crypto.create_ctr_io(Keyslot.SD, o, self.crypto.sd_path_to_iv(tmd_enc_path)) as e: 459 | e.write(bytes(cia.tmd)) 460 | 461 | # in case the contents are corrupted 462 | do_continue = False 463 | # write each content 464 | for co in cia.content_info: 465 | content_filename = co.id + '.app' 466 | if is_dlc: 467 | dir_index = format((co.cindex // 256), '08x') 468 | content_enc_path = content_root_cmd + f'/{dir_index}/{content_filename}' 469 | content_out_path = join(temp_content_root, dir_index, content_filename) 470 | else: 471 | content_enc_path = content_root_cmd + '/' + content_filename 472 | content_out_path = join(temp_content_root, content_filename) 473 | self.log(f'Writing {content_enc_path}...') 474 | with cia.open_raw_section(co.cindex) as s, open(content_out_path, 'wb') as o: 475 | result_hash = self.copy_with_progress(s, o, co.size, content_enc_path) 476 | if result_hash != co.hash: 477 | self.log(f'WARNING: Hash does not match for {content_enc_path}!') 478 | install_state['failed'].append(display_title) 479 | rename(temp_title_root, temp_title_root + '-corrupted') 480 | do_continue = True 481 | self.event.update_status(path, InstallStatus.Failed) 482 | break 483 | 484 | if do_continue: 485 | continue 486 | 487 | # generate a blank save 488 | if cia.tmd.save_size: 489 | sav_enc_path = title_root_cmd + '/data/00000001.sav' 490 | tmp_sav_out_path = join(temp_title_root, 'data', '00000001.sav') 491 | sav_out_path = join(title_root, 'data', '00000001.sav') 492 | if self.overwrite_saves or not isfile(sav_out_path): 493 | cipher = crypto.create_ctr_cipher(Keyslot.SD, crypto.sd_path_to_iv(sav_enc_path)) 494 | # in a new save, the first 0x20 are all 00s. the rest can be random 495 | data = cipher.encrypt(b'\0' * 0x20) 496 | self.log(f'Generating blank save at {sav_enc_path}...') 497 | with open(tmp_sav_out_path, 'wb') as o: 498 | o.write(data) 499 | o.write(b'\0' * (cia.tmd.save_size - 0x20)) 500 | else: 501 | self.log(f'Copying original save file from {sav_enc_path}...') 502 | copy2(sav_out_path, tmp_sav_out_path) 503 | 504 | # generate and write cmd 505 | cmd_enc_path = content_root_cmd + '/cmd/' + cmd_filename 506 | cmd_out_path = join(temp_content_root, 'cmd', cmd_filename) 507 | self.log(f'Generating {cmd_enc_path}') 508 | highest_index = 0 509 | content_ids = {} 510 | 511 | for record in cia.content_info: 512 | highest_index = record.cindex 513 | with cia.open_raw_section(record.cindex) as s: 514 | s.seek(0x100) 515 | cmac_data = s.read(0x100) 516 | 517 | id_bytes = bytes.fromhex(record.id)[::-1] 518 | cmac_data += record.cindex.to_bytes(4, 'little') + id_bytes 519 | 520 | cmac_ncch = crypto.create_cmac_object(Keyslot.CMACSDNAND) 521 | cmac_ncch.update(sha256(cmac_data).digest()) 522 | content_ids[record.cindex] = (id_bytes, cmac_ncch.digest()) 523 | 524 | # add content IDs up to the last one 525 | ids_by_index = [CMD_MISSING] * (highest_index + 1) 526 | installed_ids = [] 527 | cmacs = [] 528 | for x in range(len(ids_by_index)): 529 | try: 530 | info = content_ids[x] 531 | except KeyError: 532 | # "MISSING CONTENT!" 533 | # The 3DS does generate a cmac for missing contents, but I don't know how it works. 534 | # It doesn't matter anyway, the title seems to be fully functional. 535 | cmacs.append(bytes.fromhex('4D495353494E4720434F4E54454E5421')) 536 | else: 537 | ids_by_index[x] = info[0] 538 | cmacs.append(info[1]) 539 | installed_ids.append(info[0]) 540 | installed_ids.sort(key=lambda x: int.from_bytes(x, 'little')) 541 | 542 | final = (cmd_id.to_bytes(4, 'little') 543 | + len(ids_by_index).to_bytes(4, 'little') 544 | + len(installed_ids).to_bytes(4, 'little') 545 | + (1).to_bytes(4, 'little')) 546 | cmac_cmd_header = crypto.create_cmac_object(Keyslot.CMACSDNAND) 547 | cmac_cmd_header.update(final) 548 | final += cmac_cmd_header.digest() 549 | 550 | final += b''.join(ids_by_index) 551 | final += b''.join(installed_ids) 552 | final += b''.join(cmacs) 553 | 554 | cipher = crypto.create_ctr_cipher(Keyslot.SD, crypto.sd_path_to_iv(cmd_enc_path)) 555 | self.log(f'Writing {cmd_enc_path}') 556 | with open(cmd_out_path, 'wb') as o: 557 | o.write(cipher.encrypt(final)) 558 | 559 | # this starts building the title info entry 560 | title_info_entry_data = [ 561 | # title size 562 | title_size.to_bytes(8, 'little'), 563 | # title type, seems to usually be 0x40 564 | 0x40.to_bytes(4, 'little'), 565 | # title version 566 | int(cia.tmd.title_version).to_bytes(2, 'little'), 567 | # ncch version 568 | cia.contents[0].version.to_bytes(2, 'little'), 569 | # flags_0, only checking if there is a manual 570 | (1 if has_manual else 0).to_bytes(4, 'little'), 571 | # tmd content id, always starting with 0 572 | (0).to_bytes(4, 'little'), 573 | # cmd content id 574 | cmd_id.to_bytes(4, 'little'), 575 | # flags_1, only checking save data 576 | (1 if cia.tmd.save_size else 0).to_bytes(4, 'little'), 577 | # extdataid low 578 | extdata_id[0:4], 579 | # reserved 580 | b'\0' * 4, 581 | # flags_2, only using a common value 582 | 0x100000000.to_bytes(8, 'little'), 583 | # product code 584 | cia.contents[0].product_code.encode('ascii').ljust(0x10, b'\0'), 585 | # reserved 586 | b'\0' * 0x10, 587 | # unknown 588 | randint(0, 0xFFFFFFFF).to_bytes(4, 'little'), 589 | # reserved 590 | b'\0' * 0x2c 591 | ] 592 | 593 | self.event.update_status(path, InstallStatus.Finishing) 594 | if isdir(title_root): 595 | self.log(f'Removing original install at {title_root}...') 596 | rmtree(title_root) 597 | 598 | makedirs(tidhigh_root, exist_ok=True) 599 | rename(temp_title_root, title_root) 600 | 601 | cifinish_data[int(cia.tmd.title_id, 16)] = {'seed': (get_seed(cia.contents[0].program_id) if cia.contents[0].flags.uses_seed else None)} 602 | 603 | # This is saved regardless if any titles were installed, so the file can be upgraded just in case. 604 | save_cifinish(cifinish_path, cifinish_data) 605 | 606 | with open(join(tempdir, cia.tmd.title_id), 'wb') as o: 607 | o.write(b''.join(title_info_entry_data)) 608 | 609 | # import the directory, now including our title 610 | self.log('Importing into Title Database...') 611 | out = subprocess.run(save3ds_fuse_common_args + ['-i'], 612 | stdout=subprocess.PIPE, 613 | stderr=subprocess.STDOUT, 614 | encoding='utf-8', 615 | **extra_kwargs) 616 | if out.returncode: 617 | for l in out.stdout.split('\n'): 618 | self.log(l) 619 | self.log('Command line:') 620 | for l in pformat(out.args).split('\n'): 621 | self.log(l) 622 | install_state['failed'].append(display_title) 623 | self.event.update_status(path, InstallStatus.Failed) 624 | else: 625 | install_state['installed'].append(display_title) 626 | self.event.update_status(path, InstallStatus.Done) 627 | 628 | copied = False 629 | # launchable applications, not DLC or update data 630 | application_count = len(glob(join(tempdir, '00040000*'))) 631 | if install_state['installed']: 632 | if application_count >= 300: 633 | self.log(f'{application_count} installed applications were detected.', 1) 634 | self.log('The HOME Menu will only show 300 icons.', 1) 635 | self.log('Some applications (not updates or DLC) will need to be deleted.', 1) 636 | finalize_3dsx_orig_path = join(script_dir, 'custom-install-finalize.3dsx') 637 | hb_dir = join(self.sd, '3ds') 638 | finalize_3dsx_path = join(hb_dir, 'custom-install-finalize.3dsx') 639 | if isfile(finalize_3dsx_orig_path): 640 | self.log('Copying finalize program to ' + finalize_3dsx_path) 641 | makedirs(hb_dir, exist_ok=True) 642 | copyfile(finalize_3dsx_orig_path, finalize_3dsx_path) 643 | copied = True 644 | 645 | self.log('FINAL STEP:') 646 | self.log('Run custom-install-finalize through homebrew launcher.') 647 | self.log('This will install a ticket and seed if required.') 648 | if copied: 649 | self.log('custom-install-finalize has been copied to the SD card.') 650 | 651 | return install_state, copied, application_count 652 | 653 | def get_sd_path(self): 654 | sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex()) 655 | id1s = [] 656 | for d in scandir(sd_path): 657 | if d.is_dir() and len(d.name) == 32: 658 | try: 659 | # check if the name can be converted to hex 660 | # I'm not sure what the 3DS does if there is a folder that is not a 32-char hex string. 661 | bytes.fromhex(d.name) 662 | except ValueError: 663 | continue 664 | else: 665 | id1s.append(d.name) 666 | return [sd_path, id1s] 667 | 668 | def log(self, message, mtype=0, errorname=None, end='\n'): 669 | """Logs an Message with a type. Format is similar to python errors 670 | 671 | There are 3 types of errors, indexed accordingly 672 | type 0 = Message 673 | type 1 = Warning 674 | type 2 = Error 675 | 676 | optionally, errorname can be a custom name as a string to identify errors easily 677 | """ 678 | if errorname: 679 | errorname += ": " 680 | else: 681 | # No errorname provided 682 | errorname = "" 683 | types = [ 684 | "", # Type 0 685 | "Warning: ", # Type 1 686 | "Error: " # Type 2 687 | ] 688 | # Example: "Warning: UninformativeError: An error occured, try again."" 689 | msg_with_type = types[mtype] + errorname + str(message) 690 | self.log_lines.append(msg_with_type) 691 | self.event.on_log_msg(msg_with_type, end=end) 692 | return msg_with_type 693 | 694 | 695 | if __name__ == "__main__": 696 | parser = ArgumentParser(description='Install a CIA to the SD card for a Nintendo 3DS system.') 697 | parser.add_argument('cia', help='CIA files', nargs='+') 698 | parser.add_argument('-m', '--movable', help='movable.sed file', required=True) 699 | parser.add_argument('-b', '--boot9', help='boot9 file') 700 | parser.add_argument('-s', '--seeddb', help='seeddb file') 701 | parser.add_argument('--sd', help='path to SD root', required=True) 702 | parser.add_argument('--skip-contents', help="don't add contents, only add title info entry", action='store_true') 703 | parser.add_argument('--overwrite-saves', help='overwrite existing save files', action='store_true') 704 | parser.add_argument('--cifinish-out', help='path for cifinish.bin file, defaults to (SD root)/cifinish.bin') 705 | 706 | print(f'custom-install {CI_VERSION} - https://github.com/ihaveamac/custom-install') 707 | args = parser.parse_args() 708 | 709 | installer = CustomInstall(boot9=args.boot9, 710 | seeddb=args.seeddb, 711 | movable=args.movable, 712 | sd=args.sd, 713 | overwrite_saves=args.overwrite_saves, 714 | cifinish_out=args.cifinish_out, 715 | skip_contents=(args.skip_contents or False)) 716 | 717 | def log_handle(msg, end='\n'): 718 | print(msg, end=end) 719 | 720 | def percent_handle(total_percent, total_read, size): 721 | installer.log(f' {total_percent:>5.1f}% {total_read:>.1f} MiB / {size:.1f} MiB\r', end='') 722 | 723 | def error(exc): 724 | for line in format_exception(*exc): 725 | for line2 in line.split('\n')[:-1]: 726 | installer.log(line2) 727 | 728 | installer.event.on_log_msg += log_handle 729 | installer.event.update_percentage += percent_handle 730 | installer.event.on_error += error 731 | 732 | if not installer.check_for_id0(): 733 | installer.event.on_error(f'Could not find id0 directory {installer.crypto.id0.hex()} ' 734 | f'inside Nintendo 3DS directory.') 735 | 736 | installer.prepare_titles(args.cia) 737 | 738 | if not args.skip_contents: 739 | total_size, free_space = installer.check_size() 740 | if total_size > free_space: 741 | installer.event.on_log_msg(f'Not enough free space.\n' 742 | f'Combined title install size: {total_size / (1024 * 1024):0.2f} MiB\n' 743 | f'Free space: {free_space / (1024 * 1024):0.2f} MiB') 744 | sys.exit(1) 745 | 746 | result, copied_3dsx, application_count = installer.start() 747 | if result is False: 748 | # save3ds_fuse failed 749 | installer.log('NOTE: Once save3ds_fuse is fixed, run the same command again with --skip-contents') 750 | if application_count >= 300: 751 | installer.log(f'\n\nWarning: {application_count} installed applications were detected.\n' 752 | f'The HOME Menu will only show 300 icons.\n' 753 | f'Some applications (not updates or DLC) will need to be deleted.') 754 | --------------------------------------------------------------------------------