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];
}