From 29d8a185f96adfc8b9f4ea771d687ef0613a865a Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:18:29 +0700 Subject: [PATCH] fix: handle nested legacy iOS Documents path in validation Detect and recover from stale sandbox container paths embedded inside the current Documents directory. Extracts helper functions for path suffix normalization and joining to reduce duplication. --- lib/utils/file_access.dart | 50 +++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/lib/utils/file_access.dart b/lib/utils/file_access.dart index 026fa57e..f686bc46 100644 --- a/lib/utils/file_access.dart +++ b/lib/utils/file_access.dart @@ -20,6 +20,22 @@ final _iosLegacyRelativeDocumentsPattern = RegExp( r'^Data/Application/[A-F0-9\-]+/Documents(?:/(.*))?$', caseSensitive: false, ); +final _iosNestedLegacyDocumentsPattern = RegExp( + r'/Documents/Data/Application/[A-F0-9\-]+/Documents(?:/(.*))?$', + caseSensitive: false, +); + +String _normalizeRecoveredIosSuffix(String suffix) { + final trimmed = suffix.trim(); + if (trimmed.isEmpty) return ''; + return trimmed.startsWith('/') ? trimmed.substring(1) : trimmed; +} + +String _joinRecoveredIosPath(String documentsPath, String suffix) { + final normalizedSuffix = _normalizeRecoveredIosSuffix(suffix); + if (normalizedSuffix.isEmpty) return documentsPath; + return '$documentsPath/$normalizedSuffix'; +} /// Checks if a path is a valid writable directory on iOS. /// Returns false if: @@ -43,6 +59,12 @@ bool isValidIosWritablePath(String path) { return false; } + // Reject stale paths where an old sandbox container path has been embedded + // inside the current Documents directory. + if (_iosNestedLegacyDocumentsPattern.hasMatch(path)) { + return false; + } + // Ensure path contains a valid subdirectory (Documents, tmp, Library, etc.) // This handles cases where FilePicker returns container root final containerPattern = RegExp( @@ -70,11 +92,19 @@ Future validateOrFixIosPath( if (!Platform.isIOS) return path; final trimmed = path.trim(); + final docDir = await getApplicationDocumentsDirectory(); + + final nestedLegacyMatch = _iosNestedLegacyDocumentsPattern.firstMatch( + trimmed, + ); + if (nestedLegacyMatch != null) { + return _joinRecoveredIosPath(docDir.path, nestedLegacyMatch.group(1) ?? ''); + } + if (isValidIosWritablePath(trimmed)) { return trimmed; } - final docDir = await getApplicationDocumentsDirectory(); final candidates = []; if (trimmed.isNotEmpty) { @@ -92,14 +122,8 @@ Future validateOrFixIosPath( trimmed, ); if (legacyRelativeMatch != null) { - final suffix = (legacyRelativeMatch.group(1) ?? '').trim(); - final normalizedSuffix = suffix.startsWith('/') - ? suffix.substring(1) - : suffix; candidates.add( - normalizedSuffix.isEmpty - ? docDir.path - : '${docDir.path}/$normalizedSuffix', + _joinRecoveredIosPath(docDir.path, legacyRelativeMatch.group(1) ?? ''), ); } @@ -109,7 +133,7 @@ Future validateOrFixIosPath( final index = trimmed.indexOf(documentsMarker); if (index >= 0) { final suffix = trimmed.substring(index + documentsMarker.length).trim(); - candidates.add(suffix.isEmpty ? docDir.path : '${docDir.path}/$suffix'); + candidates.add(_joinRecoveredIosPath(docDir.path, suffix)); } } @@ -181,6 +205,14 @@ IosPathValidationResult validateIosPath(String path) { ); } + if (_iosNestedLegacyDocumentsPattern.hasMatch(path)) { + return const IosPathValidationResult( + isValid: false, + errorReason: + 'Invalid iOS app folder path. Please choose App Documents or another local folder.', + ); + } + // Check for container root without subdirectory final containerPattern = RegExp( r'/var/mobile/Containers/Data/Application/[A-F0-9\-]+',