From fbd001298b44b817cad04d258b2cf9a675b67597 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Tue, 23 Jun 2026 18:03:24 +0200 Subject: [PATCH] Validate iOS backup path before checks --- src/mvt/ios/cli.py | 5 +++- src/mvt/ios/cmd_check_backup.py | 47 +++++++++++++++++++++++++++++++++ tests/test_check_ios_backup.py | 17 ++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/mvt/ios/cli.py b/src/mvt/ios/cli.py index 446a0e6..98f47f1 100644 --- a/src/mvt/ios/cli.py +++ b/src/mvt/ios/cli.py @@ -254,7 +254,10 @@ def check_backup( cmd.list_modules() return - log.info("Checking iTunes backup located at: %s", backup_path) + if not cmd.resolve_backup_path(): + ctx.exit(1) + + log.info("Checking iTunes backup located at: %s", cmd.target_path) cmd.run() cmd.show_alerts_brief() diff --git a/src/mvt/ios/cmd_check_backup.py b/src/mvt/ios/cmd_check_backup.py index 9200964..9b01c82 100644 --- a/src/mvt/ios/cmd_check_backup.py +++ b/src/mvt/ios/cmd_check_backup.py @@ -4,6 +4,7 @@ # https://license.mvt.re/1.1/ import logging +import os from typing import Optional from mvt.common.command import Command @@ -15,6 +16,12 @@ from .modules.mixed import MIXED_MODULES log = logging.getLogger(__name__) +def is_ios_backup_folder(path: str) -> bool: + return os.path.isfile(os.path.join(path, "Manifest.db")) and os.path.isfile( + os.path.join(path, "Info.plist") + ) + + class CmdIOSCheckBackup(Command): def __init__( self, @@ -48,5 +55,45 @@ class CmdIOSCheckBackup(Command): self.name = "check-backup" self.modules = BACKUP_MODULES + MIXED_MODULES + def resolve_backup_path(self) -> bool: + if not self.target_path: + return False + + if is_ios_backup_folder(self.target_path): + return True + + if not os.path.isdir(self.target_path): + self.log.critical( + "%s does not appear to be an iTunes backup folder. " + "Expected Manifest.db and Info.plist.", + self.target_path, + ) + return False + + candidates = [] + for entry_name in sorted(os.listdir(self.target_path)): + entry_path = os.path.join(self.target_path, entry_name) + if os.path.isdir(entry_path) and is_ios_backup_folder(entry_path): + candidates.append(entry_path) + + if len(candidates) == 1: + self.log.info("Found iTunes backup in subfolder: %s", candidates[0]) + self.target_path = candidates[0] + return True + + if candidates: + self.log.critical( + "Found multiple iTunes backups in %s. Please specify one backup folder.", + self.target_path, + ) + return False + + self.log.critical( + "%s does not appear to be an iTunes backup folder. " + "Expected Manifest.db and Info.plist.", + self.target_path, + ) + return False + def module_init(self, module): module.is_backup = True diff --git a/tests/test_check_ios_backup.py b/tests/test_check_ios_backup.py index a8ad6b2..eb10b13 100644 --- a/tests/test_check_ios_backup.py +++ b/tests/test_check_ios_backup.py @@ -3,6 +3,8 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ +import shutil + from click.testing import CliRunner from mvt.ios.cli import check_backup @@ -16,3 +18,18 @@ class TestCheckBackupCommand: path = get_ios_backup_folder() result = runner.invoke(check_backup, [path]) assert result.exit_code == 0 + + def test_check_finds_backup_in_subfolder(self, tmp_path, caplog): + runner = CliRunner() + backup_path = tmp_path / "MobileSync" / "Backup" / "device-id" + shutil.copytree(get_ios_backup_folder(), backup_path) + + result = runner.invoke(check_backup, [str(backup_path.parent)]) + assert result.exit_code == 0 + assert f"Found iTunes backup in subfolder: {backup_path}" in caplog.text + + def test_check_rejects_non_backup_folder(self, tmp_path, caplog): + runner = CliRunner() + result = runner.invoke(check_backup, [str(tmp_path)]) + assert result.exit_code == 1 + assert "does not appear to be an iTunes backup folder" in caplog.text