import '../services/adb_service.dart'; import 'adb_device.dart'; import 'pending_install.dart'; enum InstallAttemptOutcome { success, failure } class InstallAttemptResult { const InstallAttemptResult({ required this.pendingInstall, required this.device, required this.result, }); final PendingInstall pendingInstall; final AdbDevice device; final AdbInstallResult result; InstallAttemptOutcome get outcome => result.succeeded ? InstallAttemptOutcome.success : InstallAttemptOutcome.failure; bool get succeeded => outcome == InstallAttemptOutcome.success; bool get failed => outcome == InstallAttemptOutcome.failure; String get artifactLabel => '#${pendingInstall.buildNumber} · ${pendingInstall.jobName} · ' '${pendingInstall.fileName}'; String get outputSummary { final stdout = result.stdout.trim(); final stderr = result.stderr.trim(); if (succeeded) { return [stdout, stderr].where((it) => it.isNotEmpty).join('\n'); } if (stderr.isNotEmpty) return _firstNonEmptyLine(stderr); if (stdout.isNotEmpty) return _firstNonEmptyLine(stdout); return '설치 실패'; } String get maskedOutputSummary => _maskSensitive(outputSummary); String get maskedDeviceSerial => _maskSerial(device.serial); String get maskedReport { final lines = [ 'AppSok 설치 리포트', '결과: ${succeeded ? '성공' : '실패'}', 'exitCode: ${result.exitCode}', 'Build: $artifactLabel', 'Device: ${device.displayName} ($maskedDeviceSerial)', if (maskedOutputSummary.isNotEmpty) '요약: $maskedOutputSummary', if (result.stdout.trim().isNotEmpty) 'stdout:\n${_maskSensitive(result.stdout.trim())}', if (result.stderr.trim().isNotEmpty) 'stderr:\n${_maskSensitive(result.stderr.trim())}', ]; return lines.join('\n'); } String _firstNonEmptyLine(String text) { final lines = text .split('\n') .map((line) => line.trim()) .where((line) => line.isNotEmpty); return lines.isEmpty ? '' : lines.first; } String _maskSensitive(String value) { final masked = value .replaceAll(RegExp(r'https?:\/\/[^\s]+'), '[masked-url]') .replaceAllMapped( RegExp( r'\b(authorization)\s*[:=]\s*bearer\s+[A-Za-z0-9._~+/=-]+', caseSensitive: false, ), (match) => '${match.group(1)}: [masked]', ) .replaceAllMapped( RegExp( r'\b(api[_-]?token|token|password|passwd|secret)\s*[:=]\s*[^\s&]+', caseSensitive: false, ), (match) => '${match.group(1)}=[masked]', ); return device.serial.isEmpty ? masked : masked.replaceAll(device.serial, maskedDeviceSerial); } String _maskSerial(String serial) { if (serial.isEmpty) return '[masked-serial]'; if (serial.length <= 4) return '*' * serial.length; final prefix = serial.substring(0, 2); final suffix = serial.substring(serial.length - 2); return '$prefix${'*' * (serial.length - 4)}$suffix'; } }