-
-
Notifications
You must be signed in to change notification settings - Fork 0
⚡ Optimize 3DS/CIA Decryption with Parallel Processing #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…ironments Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
🤖 Hi @Ven0m0, I've received your request, and I'm working on it now! You can track my progress in the logs for more details. |
Summary of ChangesHello @Ven0m0, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request dramatically improves the performance of the 3DS/CIA decryption script by introducing parallel processing. Previously, the script processed files sequentially, underutilizing system resources. The core change involves refactoring the decryption logic to execute tasks concurrently within isolated environments, ensuring stability and correctness while achieving a substantial speedup. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
|
🤖 I'm sorry @Ven0m0, but I was unable to process your request. Please see the logs for more details. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request optimizes the 3DS/CIA decryption script by introducing parallel processing using ThreadPoolExecutor for faster multi-file processing. However, a critical argument injection vulnerability has been identified in the way command-line arguments are constructed for the makerom tool. The use of .split() on strings with potentially user-controlled spaces in quoted paths can lead to incorrect argument partitioning and allow for the injection of arbitrary flags. It is strongly recommended to refactor the argument-building helper functions to return lists of arguments instead of strings to address this. Additionally, the review includes a critical fix for a syntax error in the new benchmark script, a suggestion to improve error logging in the parallel processing logic, and a minor cleanup in the benchmark script's path handling.
| try: | ||
| cia_3ds_decryptor.main() | ||
| except SystemExit as e: | ||
| if e.code != 0: | ||
| print(f"Script exited with code {e.code}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This try...except block is incorrectly indented, which will cause a SyntaxError: unexpected indent. It should be de-indented to be correctly scoped within the with statement.
| try: | |
| cia_3ds_decryptor.main() | |
| except SystemExit as e: | |
| if e.code != 0: | |
| print(f"Script exited with code {e.code}") | |
| try: | |
| cia_3ds_decryptor.main() | |
| except SystemExit as e: | |
| if e.code != 0: | |
| print(f"Script exited with code {e.code}") |
| "p", | ||
| "-o", | ||
| str(out_cci), | ||
| ] + arg_str.split() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of arg_str.split() here is vulnerable to argument injection and will fail for filenames containing spaces. The arg_str is constructed by build_ncch_args which wraps paths in double quotes, but .split() does not respect these quotes, causing arguments to be incorrectly partitioned. Since filenames are only partially sanitized (allowing spaces and hyphens), an attacker could craft a filename to inject arbitrary flags into the makerom command.
To remediate this, refactor build_ncch_args to return a list of arguments and append it directly to the cmd list without using split().
| cmd = ["-f", "cia", "-ignoresign", "-target", "p", "-o", str(out_cia)] | ||
| if cia_type == "DLC": | ||
| cmd.append("-dlc") | ||
| cmd.extend(arg_str.split() + ["-ver", info.title_version]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the issue in decrypt_3ds, using arg_str.split() here allows for argument injection when filenames contain spaces. The helper functions build_ncch_args_contentid and build_ncch_args_sequential return strings with quoted paths that are incorrectly split by .split(). This can be exploited by crafting filenames that include spaces and command-line flags.
Remediation: Refactor the helper functions to return lists of arguments and avoid the use of split() at the call site.
|
|
||
| # Add current dir to path to import the script | ||
| sys.path.append(os.path.dirname(os.path.abspath(__file__))) | ||
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| futures = [] | ||
| with concurrent.futures.ThreadPoolExecutor() as executor: | ||
| if cnt.count_3ds: | ||
| logging.info("[i] Found %d 3DS file(s). Start decrypting...", cnt.count_3ds) | ||
| for f in sorted(root.glob("*.3ds")): | ||
| futures.append( | ||
| executor.submit( | ||
| process_file_task, decrypt_3ds, root, f, tools_list, seeddb | ||
| ) | ||
| ) | ||
|
|
||
| if cnt.count_cia: | ||
| logging.info("[i] Found %d CIA file(s). Start decrypting...", cnt.count_cia) | ||
| for f in sorted(root.glob("*.cia")): | ||
| futures.append( | ||
| executor.submit( | ||
| process_file_task, decrypt_cia, root, f, tools_list, seeddb | ||
| ) | ||
| ) | ||
|
|
||
| # Wait for completion and accumulate results | ||
| for future in concurrent.futures.as_completed(futures): | ||
| try: | ||
| result_cnt = future.result() | ||
| cnt += result_cnt | ||
| except Exception as e: | ||
| logging.error(f"Task failed with exception: {e}") | ||
| # Optionally update error counts here if we knew which type of file failed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling for failed tasks could be improved to identify which file caused the failure. By changing futures from a list to a dictionary that maps futures to file paths, you can provide more specific error messages, which will greatly aid debugging.
futures = {}
with concurrent.futures.ThreadPoolExecutor() as executor:
if cnt.count_3ds:
logging.info("[i] Found %d 3DS file(s). Start decrypting...", cnt.count_3ds)
for f in sorted(root.glob("*.3ds")):
future = executor.submit(
process_file_task, decrypt_3ds, root, f, tools_list, seeddb
)
futures[future] = f
if cnt.count_cia:
logging.info("[i] Found %d CIA file(s). Start decrypting...", cnt.count_cia)
for f in sorted(root.glob("*.cia")):
future = executor.submit(
process_file_task, decrypt_cia, root, f, tools_list, seeddb
)
futures[future] = f
# Wait for completion and accumulate results
for future in concurrent.futures.as_completed(futures):
file_path = futures[future]
try:
result_cnt = future.result()
cnt += result_cnt
except Exception as e:
logging.error(f"Task for file '{file_path.name}' failed with exception: {e}")
# Optionally update error counts here if we knew which type of file failedThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR refactors the CIA/3DS decryption script to use parallel processing with ThreadPoolExecutor, aiming to improve performance by processing multiple files concurrently. The implementation creates isolated temporary environments for each decryption task to prevent race conditions with external tools that rely on shared side effects.
Changes:
- Implemented parallel processing using ThreadPoolExecutor for concurrent file decryption
- Added
prepare_task_envcontext manager to create isolated temporary directories for each task - Implemented
Counters.__add__to aggregate results from parallel tasks - Fixed numerous formatting issues and improved code readability
- Added
benchmark_decryptor.pyscript to measure performance improvements
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 12 comments.
| File | Description |
|---|---|
| cia_3ds_decryptor.py | Main refactoring: added parallel processing infrastructure, isolated task environments, counter aggregation, and code formatting improvements |
| benchmark_decryptor.py | New benchmark script with mocked environment to measure performance gains |
| for f in sorted(root.glob("*.3ds")): | ||
| futures.append( | ||
| executor.submit( | ||
| process_file_task, decrypt_3ds, root, f, tools_list, seeddb | ||
| ) | ||
| ) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop submits tasks for ALL *.3ds files, including those that are already decrypted (with "-decrypted" in the name). However, the count_3ds only includes non-decrypted files. This means already-decrypted files will be submitted for processing (though decrypt_3ds will skip them early), and more importantly, will be submitted BEFORE any filtering is applied. This inconsistency could lead to unexpected behavior and wasted thread resources. The loop should filter out already-decrypted files to match the counting logic.
| for f in sorted(root.glob("*.cia")): | ||
| futures.append( | ||
| executor.submit( | ||
| process_file_task, decrypt_cia, root, f, tools_list, seeddb | ||
| ) | ||
| ) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop submits tasks for ALL *.cia files, including those that are already decrypted (with "-decrypted" in the name). However, the count_cia only includes non-decrypted files. This inconsistency could lead to unexpected behavior and wasted thread resources processing files that should be skipped. The loop should filter out already-decrypted files to match the counting logic.
| futures = [] | ||
| with concurrent.futures.ThreadPoolExecutor() as executor: | ||
| if cnt.count_3ds: | ||
| logging.info("[i] Found %d 3DS file(s). Start decrypting...", cnt.count_3ds) | ||
| for f in sorted(root.glob("*.3ds")): | ||
| futures.append( | ||
| executor.submit( | ||
| process_file_task, decrypt_3ds, root, f, tools_list, seeddb | ||
| ) | ||
| ) | ||
|
|
||
| if cnt.count_cia: | ||
| logging.info("[i] Found %d CIA file(s). Start decrypting...", cnt.count_cia) | ||
| for f in sorted(root.glob("*.cia")): | ||
| futures.append( | ||
| executor.submit( | ||
| process_file_task, decrypt_cia, root, f, tools_list, seeddb | ||
| ) | ||
| ) | ||
|
|
||
| # Wait for completion and accumulate results | ||
| for future in concurrent.futures.as_completed(futures): | ||
| try: | ||
| result_cnt = future.result() | ||
| cnt += result_cnt | ||
| except Exception as e: | ||
| logging.error(f"Task failed with exception: {e}") | ||
| # Optionally update error counts here if we knew which type of file failed | ||
|
|
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exception handling is too broad and silently continues after logging. If a task fails with an exception, the error counters (cia_err, ds_err, cci_err) are not updated because the exception is caught before the function can update its local_cnt. This means failed tasks won't be reflected in the final error statistics. Consider updating the appropriate error counter when catching exceptions, or at minimum, increment a general error counter.
| futures = [] | |
| with concurrent.futures.ThreadPoolExecutor() as executor: | |
| if cnt.count_3ds: | |
| logging.info("[i] Found %d 3DS file(s). Start decrypting...", cnt.count_3ds) | |
| for f in sorted(root.glob("*.3ds")): | |
| futures.append( | |
| executor.submit( | |
| process_file_task, decrypt_3ds, root, f, tools_list, seeddb | |
| ) | |
| ) | |
| if cnt.count_cia: | |
| logging.info("[i] Found %d CIA file(s). Start decrypting...", cnt.count_cia) | |
| for f in sorted(root.glob("*.cia")): | |
| futures.append( | |
| executor.submit( | |
| process_file_task, decrypt_cia, root, f, tools_list, seeddb | |
| ) | |
| ) | |
| # Wait for completion and accumulate results | |
| for future in concurrent.futures.as_completed(futures): | |
| try: | |
| result_cnt = future.result() | |
| cnt += result_cnt | |
| except Exception as e: | |
| logging.error(f"Task failed with exception: {e}") | |
| # Optionally update error counts here if we knew which type of file failed | |
| futures = {} | |
| with concurrent.futures.ThreadPoolExecutor() as executor: | |
| if cnt.count_3ds: | |
| logging.info("[i] Found %d 3DS file(s). Start decrypting...", cnt.count_3ds) | |
| for f in sorted(root.glob("*.3ds")): | |
| future = executor.submit( | |
| process_file_task, decrypt_3ds, root, f, tools_list, seeddb | |
| ) | |
| futures[future] = "3ds" | |
| if cnt.count_cia: | |
| logging.info("[i] Found %d CIA file(s). Start decrypting...", cnt.count_cia) | |
| for f in sorted(root.glob("*.cia")): | |
| future = executor.submit( | |
| process_file_task, decrypt_cia, root, f, tools_list, seeddb | |
| ) | |
| futures[future] = "cia" | |
| # Wait for completion and accumulate results | |
| for future in concurrent.futures.as_completed(futures): | |
| task_type = futures.get(future) | |
| try: | |
| result_cnt = future.result() | |
| cnt += result_cnt | |
| except Exception as e: | |
| logging.error(f"Task failed with exception: {e}") | |
| # Update error counts so failed tasks are reflected in statistics | |
| if task_type == "3ds": | |
| cnt.ds_err += 1 | |
| elif task_type == "cia": | |
| cnt.cia_err += 1 | |
| # If task_type is None or unrecognized, we intentionally do not guess |
| for f in sorted(root.glob("*-decrypted.cia")): | ||
| convert_cia_to_cci(root, f, makerom, cnt) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The convert_cia_to_cci function is still running sequentially and directly mutates the shared cnt object. Unlike the parallel decrypt operations, this function doesn't create isolated environments. While this may be safe if makerom doesn't create side effects in bin_dir, it creates an inconsistent pattern where some operations are parallelized with isolation and others aren't. If makerom is safe to run concurrently, consider parallelizing this as well for consistency and potential performance gains. If it's not safe, document why.
| for future in concurrent.futures.as_completed(futures): | ||
| try: | ||
| result_cnt = future.result() | ||
| cnt += result_cnt |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of += operator with the Counters dataclass creates a new Counters object rather than mutating the original cnt object. This works, but it's inefficient as it creates new objects for each completed task. The main cnt object is being reassigned (cnt = cnt + result_cnt) which is correct, but a more efficient approach would be to implement an in-place addition method (iadd) that mutates the counter directly, or use explicit field assignment.
| def mock_run_tool(tool, args, stdin="", cwd=None): | ||
| # Simulate work | ||
| time.sleep(0.1) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The benchmark uses time.sleep(0.1) to simulate work, which will make threads yield and allow concurrent execution. However, this doesn't accurately simulate CPU-bound or I/O-bound work. Real decryption involves actual subprocess execution which behaves differently from sleep. The benchmark may show inflated speedup numbers that don't reflect real-world performance. Consider using a more realistic simulation (e.g., actual CPU work or blocking I/O) to validate the performance claims.
| result_cnt = future.result() | ||
| cnt += result_cnt | ||
| except Exception as e: | ||
| logging.error(f"Task failed with exception: {e}") |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an f-string being used for logging which is inconsistent with the rest of the codebase that uses the % operator for string formatting in logging calls. While f-strings work, they are evaluated before the logging call, which means the string formatting happens even if the log level would filter the message. Using the % style (logging.error("Task failed with exception: %s", e)) is more efficient and consistent with the rest of the file.
| logging.error(f"Task failed with exception: {e}") | |
| logging.error("Task failed with exception: %s", e) |
| def link_or_copy(src: Path, dst: Path) -> None: | ||
| if IS_WIN: | ||
| shutil.copy2(src, dst) | ||
| else: | ||
| os.symlink(src.resolve(), dst) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The link_or_copy function on Linux creates symlinks to tool executables. If the tools need to be executable (which they do, as shown by the chmod in benchmark setup), symlinks preserve the executable bit. However, if a tool is a Windows .exe file that will be run through Wine, symlinking should work fine. But if any of these tools write to relative paths or expect to be in a specific directory structure, symlinks could cause issues. Consider documenting this assumption or adding executable permission checks after linking.
| logging.basicConfig( | ||
| level=logging.INFO, | ||
| format="%(asctime)s = %(message)s", | ||
| datefmt="%Y-%m-%d - %H:%M:%S", | ||
| handlers=[logging.FileHandler(log_file, mode="w", encoding="utf-8")], | ||
| ) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logging module is not thread-safe by default when writing to file handlers. With multiple threads calling logging functions simultaneously, this can lead to garbled log output or race conditions. Consider using a QueueHandler and QueueListener for thread-safe logging in concurrent environments, or ensure the FileHandler is configured with thread-safe locks.
| except OSError: | ||
| pass |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except OSError: | |
| pass | |
| except OSError as e: | |
| logging.warning( | |
| "[^] Failed to sanitize filename '%s' -> '%s': %s", | |
| f.name, | |
| new_name, | |
| e, | |
| ) |
…ironments Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
💡 What:
cia_3ds_decryptor.pyto useThreadPoolExecutorfor parallel file processing.prepare_task_envto create isolated temporary environments for each decryption task, preventing race conditions with external tools (ctrtool,decrypt,makerom) that rely on shared side-effects (outputting tobindirectory).🎯 Why:
📊 Measured Improvement:
benchmark_decryptor.py(included).PR created automatically by Jules for task 1808481003780424717 started by @Ven0m0