using System.IO; using System.Security.Cryptography; using System.Text; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L8-1: 파일 해시 검증 핸들러. "hash" 프리픽스로 사용합니다. /// /// 예: hash → 사용법 안내 /// hash C:\file.zip → SHA256 (기본) 계산 /// hash md5 C:\file.zip → MD5 계산 /// hash sha1 C:\file.zip → SHA1 계산 /// hash sha512 C:\file.zip → SHA512 계산 /// hash check <기대값> → 클립보드의 해시값과 비교 /// 경로 미입력 시 클립보드에서 파일 경로 자동 감지. /// Enter → 해시 결과를 클립보드에 복사. /// public class FileHashHandler : IActionHandler { public string? Prefix => "hash"; public PluginMetadata Metadata => new( "FileHash", "파일 해시 검증 — MD5 · SHA1 · SHA256 · SHA512", "1.0", "AX"); private static readonly string[] Algos = ["md5", "sha1", "sha256", "sha512"]; public async Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); if (string.IsNullOrWhiteSpace(q)) { // 클립보드에 파일 경로가 있으면 자동 감지 var clipPath = GetClipboardFilePath(); if (!string.IsNullOrEmpty(clipPath)) { items.Add(new LauncherItem( $"SHA256: {Path.GetFileName(clipPath)}", clipPath, null, ("compute", "sha256", clipPath), Symbol: "\uE8C4")); foreach (var algo in Algos) items.Add(new LauncherItem( $"hash {algo}", $"{algo.ToUpperInvariant()} 계산", null, ("compute", algo, clipPath), Symbol: "\uE8C4")); } else { items.Add(new LauncherItem( "파일 해시 계산", "hash <경로> 또는 hash md5|sha1|sha256|sha512 <경로>", null, null, Symbol: "\uE8C4")); items.Add(new LauncherItem( "hash check <기대 해시값>", "클립보드의 해시와 비교 검증", null, null, Symbol: "\uE73E")); } return items; } // "check " — 클립보드 해시 비교 if (q.StartsWith("check ", StringComparison.OrdinalIgnoreCase)) { var expected = q[6..].Trim(); var clipText = GetClipboardText()?.Trim(); if (!string.IsNullOrEmpty(clipText) && !string.IsNullOrEmpty(expected)) { var match = expected.Equals(clipText, StringComparison.OrdinalIgnoreCase); items.Add(new LauncherItem( match ? "✓ 해시 일치" : "✗ 해시 불일치", $"기대값: {Truncate(expected, 40)}", null, null, Symbol: match ? "\uE73E" : "\uE711")); if (!match) items.Add(new LauncherItem( "클립보드", Truncate(clipText, 60), null, null, Symbol: "\uE8C8")); } else { items.Add(new LauncherItem( "비교 대상 없음", "먼저 해시 계산 결과를 클립보드에 복사하세요", null, null, Symbol: "\uE783")); } return items; } // 알고리즘 + 경로 파싱 string algo2 = "sha256"; string filePath = q; var parts = q.Split(' ', 2); if (parts.Length == 2 && Algos.Contains(parts[0].ToLowerInvariant())) { algo2 = parts[0].ToLowerInvariant(); filePath = parts[1].Trim().Trim('"'); } else { // 알고리즘 없이 경로만 → 모든 알고리즘 표시 filePath = q.Trim('"'); } if (!File.Exists(filePath)) { // 클립보드 경로 시도 var clipPath = GetClipboardFilePath(); if (!string.IsNullOrEmpty(clipPath) && File.Exists(clipPath)) filePath = clipPath; else { items.Add(new LauncherItem( "파일을 찾을 수 없음", filePath, null, null, Symbol: "\uE783")); return items; } } var fileName = Path.GetFileName(filePath); var fileSize = new FileInfo(filePath).Length; var sizeMb = fileSize / 1024.0 / 1024.0; if (algo2 == "sha256" && parts.Length == 1) { // 경로만 입력 → 모든 알고리즘 항목 표시 items.Add(new LauncherItem( fileName, $"{sizeMb:F1} MB", null, null, Symbol: "\uE8F4")); foreach (var a in Algos) { items.Add(new LauncherItem( a.ToUpperInvariant(), "계산 중... (Enter로 실행)", null, ("compute", a, filePath), Symbol: "\uE8C4")); } } else { // 특정 알고리즘 계산 items.Add(new LauncherItem( $"계산 중: {algo2.ToUpperInvariant()}", $"{fileName} ({sizeMb:F1} MB)", null, ("compute", algo2, filePath), Symbol: "\uE8C4")); try { var hash = await ComputeHashAsync(filePath, algo2, ct); items.Clear(); items.Add(new LauncherItem( hash, $"{algo2.ToUpperInvariant()} · {fileName}", null, ("copy", hash), Symbol: "\uE8C4")); } catch (OperationCanceledException) { } catch (Exception ex) { items.Add(new LauncherItem("해시 계산 실패", ex.Message, null, null, Symbol: "\uE783")); } } return items; } public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) { switch (item.Data) { case ("copy", string hash): TryCopyToClipboard(hash); NotificationService.Notify("FileHash", "해시를 클립보드에 복사했습니다."); break; case ("compute", string algo, string filePath): try { var hash = await ComputeHashAsync(filePath, algo, ct); TryCopyToClipboard(hash); NotificationService.Notify( $"{algo.ToUpperInvariant()} 완료", $"{Path.GetFileName(filePath)}: {Truncate(hash, 32)}…"); } catch (Exception ex) { NotificationService.Notify("FileHash 오류", ex.Message); } break; } } // ── 헬퍼 ──────────────────────────────────────────────────────────────── private static async Task ComputeHashAsync( string filePath, string algo, CancellationToken ct) { using HashAlgorithm hasher = algo.ToLowerInvariant() switch { "md5" => MD5.Create(), "sha1" => SHA1.Create(), "sha512" => SHA512.Create(), _ => SHA256.Create(), }; await using var stream = File.OpenRead(filePath); var hashBytes = await Task.Run(() => hasher.ComputeHash(stream), ct); return Convert.ToHexString(hashBytes).ToLowerInvariant(); } private static string? GetClipboardFilePath() { try { string? text = null; System.Windows.Application.Current.Dispatcher.Invoke(() => { if (Clipboard.ContainsText()) text = Clipboard.GetText()?.Trim().Trim('"'); }); return !string.IsNullOrEmpty(text) && File.Exists(text) ? text : null; } catch { return null; } } private static string? GetClipboardText() { try { string? text = null; System.Windows.Application.Current.Dispatcher.Invoke(() => { if (Clipboard.ContainsText()) text = Clipboard.GetText(); }); return text; } catch { return null; } } private static void TryCopyToClipboard(string text) { try { System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text)); } catch { /* 비핵심 */ } } private static string Truncate(string s, int max) => s.Length <= max ? s : s[..max]; }