using System.Text; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L20-1: 16진수·바이트 변환기 핸들러. "hex" 프리픽스로 사용합니다. /// /// 예: hex → 클립보드 텍스트 → hex 변환 /// hex hello → "hello" → 68 65 6C 6C 6F /// hex 68656c6c6f → hex → "hello" 디코딩 /// hex dump hello world → 헥스 덤프 형식 (오프셋·hex·ASCII) /// hex 0xFF → 0xFF = 255 (십진수·이진수·문자) /// hex add 0x1A 0x2B → hex 덧셈 /// hex xor 0xAB 0xCD → bitwise XOR /// hex and 0xFF 0x0F → bitwise AND /// hex or 0xA0 0x0F → bitwise OR /// hex not 0xFF → bitwise NOT (8비트) /// hex bytes → n바이트 크기 단위 표시 (KB·MB·GB) /// Enter → 결과 복사. /// public class HexHandler : IActionHandler { public string? Prefix => "hex"; public PluginMetadata Metadata => new( "Hex", "16진수·바이트 변환기 — 텍스트↔hex·덤프·비트연산·크기 단위", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); // 클립보드 읽기 string? clipboard = null; try { System.Windows.Application.Current.Dispatcher.Invoke(() => { if (Clipboard.ContainsText()) clipboard = Clipboard.GetText().Trim(); }); } catch { } if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("16진수·바이트 변환기", "hex <텍스트> / hex / hex dump / hex 0xFF / hex bytes ", null, null, Symbol: "\uE8EF")); if (!string.IsNullOrWhiteSpace(clipboard)) items.AddRange(BuildFromText(clipboard!, brief: true)); return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); // hex dump if (sub == "dump") { var text = parts.Length > 1 ? string.Join(" ", parts[1..]) : clipboard ?? ""; if (string.IsNullOrEmpty(text)) { items.Add(ErrorItem("텍스트를 입력하거나 클립보드에 복사하세요")); } else items.AddRange(BuildDump(text)); return Task.FromResult>(items); } // hex bytes if (sub == "bytes" && parts.Length >= 2 && long.TryParse(parts[1], out var byteCount)) { items.AddRange(BuildByteSize(byteCount)); return Task.FromResult>(items); } // hex add/xor/and/or/not (비트 연산) if (sub is "add" or "xor" or "and" or "or" or "not") { items.AddRange(BuildBitOp(sub, parts)); return Task.FromResult>(items); } // 단일 hex 값 (0xFF, 0xAB, FF, AB...) if (TryParseHexValue(parts[0], out var hexVal)) { items.AddRange(BuildFromHexValue(hexVal, parts[0])); return Task.FromResult>(items); } // 순수 hex 문자열인지 판단 (2자 이상, 모두 hex digit, 짝수 길이) var raw = parts[0].Replace(" ", "").Replace("-", "").Replace(":", ""); if (raw.Length >= 2 && raw.Length % 2 == 0 && IsAllHex(raw)) { items.AddRange(BuildFromHexString(raw)); return Task.FromResult>(items); } // 일반 텍스트 → hex 변환 var input = string.Join(" ", parts); items.AddRange(BuildFromText(input, brief: false)); return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is ("copy", string text)) { try { System.Windows.Application.Current.Dispatcher.Invoke( () => Clipboard.SetText(text)); NotificationService.Notify("Hex", "클립보드에 복사했습니다."); } catch { } } return Task.CompletedTask; } // ── 빌더 ──────────────────────────────────────────────────────────────── private static IEnumerable BuildFromText(string text, bool brief) { var bytes = Encoding.UTF8.GetBytes(text); var hex = BitConverter.ToString(bytes).Replace("-", ""); var spaced = string.Join(" ", Enumerable.Range(0, hex.Length / 2) .Select(i => hex.Substring(i * 2, 2))); yield return new LauncherItem(spaced, $"텍스트 → Hex ({bytes.Length} bytes) · Enter 복사", null, ("copy", spaced), Symbol: "\uE8EF"); if (!brief) { yield return CopyItem("공백 없음", hex); yield return CopyItem("소문자", hex.ToLowerInvariant()); yield return CopyItem("0x 접두사", string.Join(" ", Enumerable.Range(0, hex.Length / 2) .Select(i => "0x" + hex.Substring(i * 2, 2)))); yield return CopyItem("바이트 수", $"{bytes.Length} bytes"); var b64 = Convert.ToBase64String(bytes); yield return CopyItem("Base64", b64); } } private static IEnumerable BuildFromHexString(string hex) { hex = hex.ToUpperInvariant(); byte[]? bytes = null; string? parseError = null; try { bytes = Enumerable.Range(0, hex.Length / 2) .Select(i => Convert.ToByte(hex.Substring(i * 2, 2), 16)) .ToArray(); } catch { parseError = "올바른 hex 문자열이 아닙니다"; } if (parseError != null) { yield return ErrorItem(parseError); yield break; } var safeBytes = bytes!; var utf8 = TrySafeUtf8(safeBytes); var ascii = TrySafeAscii(safeBytes); yield return new LauncherItem($"Hex → 텍스트", $"{safeBytes.Length} bytes · UTF-8 디코딩", null, null, Symbol: "\uE8EF"); if (utf8 != null) yield return CopyItem("UTF-8 텍스트", utf8); if (ascii != null && ascii != utf8) yield return CopyItem("ASCII 텍스트", ascii); yield return CopyItem("바이트 수", $"{safeBytes.Length}"); // 숫자 해석 (최대 8바이트) if (safeBytes.Length <= 8) { var padded = new byte[8]; Buffer.BlockCopy(safeBytes, 0, padded, 8 - safeBytes.Length, safeBytes.Length); var bigEndian = BitConverter.IsLittleEndian ? BitConverter.ToUInt64(padded.Reverse().ToArray()) : BitConverter.ToUInt64(padded); yield return CopyItem($"정수 (big-endian)", bigEndian.ToString()); } } private static IEnumerable BuildFromHexValue(ulong val, string original) { yield return new LauncherItem($"{original} = {val}", $"16진수 → 십진수 · Enter 복사", null, ("copy", val.ToString()), Symbol: "\uE8EF"); yield return CopyItem("십진수", val.ToString()); yield return CopyItem("16진수", $"0x{val:X}"); yield return CopyItem("8진수", $"0o{Convert.ToString((long)val, 8)}"); yield return CopyItem("이진수", $"0b{Convert.ToString((long)val, 2)}"); if (val <= 127) yield return CopyItem("ASCII 문자", ((char)val).ToString()); yield return CopyItem("NOT (64bit)", $"0x{(~val):X16}"); } private static IEnumerable BuildDump(string text) { var bytes = Encoding.UTF8.GetBytes(text); var lines = new List(); var sb = new StringBuilder(); for (int i = 0; i < bytes.Length; i += 16) { var chunk = bytes.Skip(i).Take(16).ToArray(); var hexPart = string.Join(" ", chunk.Select(b => $"{b:X2}")).PadRight(47); var asciiPart = new string(chunk.Select(b => b >= 32 && b < 127 ? (char)b : '.').ToArray()); var line = $"{i:X8} {hexPart} |{asciiPart}|"; lines.Add(line); } var dump = string.Join("\n", lines); yield return new LauncherItem("헥스 덤프", $"{bytes.Length} bytes · {lines.Count} 행", null, ("copy", dump), Symbol: "\uE8EF"); foreach (var line in lines.Take(8)) yield return new LauncherItem(line, "", null, ("copy", line), Symbol: "\uE8EF"); if (lines.Count > 8) yield return new LauncherItem($"... ({lines.Count - 8}행 더 있음)", "전체 복사하려면 상단 항목 Enter", null, null, Symbol: "\uE8EF"); } private static IEnumerable BuildBitOp(string op, string[] parts) { if (op == "not") { if (parts.Length < 2 || !TryParseHexValue(parts[1], out var v)) { yield return ErrorItem("예: hex not 0xFF"); yield break; } var r8 = (byte)(~(byte)v); var r16 = (ushort)(~(ushort)v); yield return new LauncherItem($"NOT 0x{v:X} = 0x{r8:X2} (8bit) / 0x{r16:X4} (16bit)", "비트 반전", null, ("copy", $"0x{r8:X2}"), Symbol: "\uE8EF"); yield return CopyItem("NOT 8bit", $"0x{r8:X2} ({r8})"); yield return CopyItem("NOT 16bit", $"0x{r16:X4} ({r16})"); yield return CopyItem("NOT 64bit", $"0x{(~v):X16}"); yield break; } if (parts.Length < 3 || !TryParseHexValue(parts[1], out var a) || !TryParseHexValue(parts[2], out var b)) { yield return ErrorItem($"예: hex {op} 0xAB 0xCD"); yield break; } ulong result = op switch { "add" => a + b, "xor" => a ^ b, "and" => a & b, "or" => a | b, _ => 0 }; var symbol = op switch { "add" => "+", "xor" => "^", "and" => "&", "or" => "|", _ => "?" }; yield return new LauncherItem($"0x{a:X} {symbol} 0x{b:X} = 0x{result:X}", $"{a} {symbol} {b} = {result} · Enter 복사", null, ("copy", $"0x{result:X}"), Symbol: "\uE8EF"); yield return CopyItem("16진수", $"0x{result:X}"); yield return CopyItem("십진수", result.ToString()); yield return CopyItem("이진수", $"0b{Convert.ToString((long)result, 2)}"); } private static IEnumerable BuildByteSize(long bytes) { yield return new LauncherItem($"{bytes:N0} bytes", "크기 단위 변환 · Enter 복사", null, ("copy", bytes.ToString()), Symbol: "\uE8EF"); yield return CopyItem("Bytes", $"{bytes:N0}"); yield return CopyItem("KB (1000)", $"{bytes / 1000.0:F3}"); yield return CopyItem("KiB (1024)",$"{bytes / 1024.0:F3}"); yield return CopyItem("MB (1000)", $"{bytes / 1_000_000.0:F3}"); yield return CopyItem("MiB (1024)",$"{bytes / 1_048_576.0:F3}"); yield return CopyItem("GB (1000)", $"{bytes / 1_000_000_000.0:F4}"); yield return CopyItem("GiB (1024)",$"{bytes / 1_073_741_824.0:F4}"); if (bytes >= 1_000_000_000_000L) yield return CopyItem("TB (1000)", $"{bytes / 1_000_000_000_000.0:F4}"); } // ── 헬퍼 ──────────────────────────────────────────────────────────────── private static bool TryParseHexValue(string s, out ulong val) { val = 0; s = s.Trim(); if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) return ulong.TryParse(s[2..], System.Globalization.NumberStyles.HexNumber, null, out val); if (s.StartsWith("0X", StringComparison.OrdinalIgnoreCase)) return ulong.TryParse(s[2..], System.Globalization.NumberStyles.HexNumber, null, out val); return false; } private static bool IsAllHex(string s) => s.All(c => c is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F'); private static string? TrySafeUtf8(byte[] b) { try { var s = Encoding.UTF8.GetString(b); return s; } catch { return null; } } private static string? TrySafeAscii(byte[] b) { try { return Encoding.ASCII.GetString(b); } catch { return null; } } private static LauncherItem CopyItem(string label, string value) => new(label, value, null, ("copy", value), Symbol: "\uE8EF"); private static LauncherItem ErrorItem(string msg) => new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783"); }