using System.IO; using System.Text; using System.Text.Json; namespace AxCopilot.Services.Agent; /// 파일 인코딩 감지 및 변환 도구. public class EncodingTool : IAgentTool { public string Name => "encoding_tool"; public string Description => "Detect and convert file text encoding. Actions: " + "'detect' — detect file encoding (UTF-8, EUC-KR, etc.); " + "'convert' — convert file from one encoding to another; " + "'list' — list common encoding names."; public ToolParameterSchema Parameters => new() { Properties = new() { ["action"] = new() { Type = "string", Description = "Action: detect, convert, list", Enum = ["detect", "convert", "list"], }, ["path"] = new() { Type = "string", Description = "File path", }, ["from_encoding"] = new() { Type = "string", Description = "Source encoding name (e.g. 'euc-kr', 'shift-jis'). Auto-detected if omitted.", }, ["to_encoding"] = new() { Type = "string", Description = "Target encoding name (default: 'utf-8')", }, }, Required = ["action"], }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { var action = args.GetProperty("action").GetString() ?? ""; if (action == "list") return ListEncodings(); var rawPath = args.TryGetProperty("path", out var pv) ? pv.GetString() ?? "" : ""; if (string.IsNullOrEmpty(rawPath)) return ToolResult.Fail("'path'가 필요합니다."); var path = Path.IsPathRooted(rawPath) ? rawPath : Path.Combine(context.WorkFolder, rawPath); if (!context.IsPathAllowed(path)) return ToolResult.Fail($"경로 접근 차단: {path}"); if (!File.Exists(path)) return ToolResult.Fail($"파일 없음: {path}"); try { return action switch { "detect" => DetectEncoding(path), "convert" => await ConvertEncoding(path, args, context), _ => ToolResult.Fail($"Unknown action: {action}"), }; } catch (Exception ex) { return ToolResult.Fail($"인코딩 처리 오류: {ex.Message}"); } } private static ToolResult DetectEncoding(string path) { var bytes = File.ReadAllBytes(path); var detected = DetectEncodingFromBytes(bytes); var sb = new StringBuilder(); sb.AppendLine($"File: {Path.GetFileName(path)}"); sb.AppendLine($"Size: {bytes.Length:N0} bytes"); sb.AppendLine($"Detected Encoding: {detected.EncodingName}"); sb.AppendLine($"Code Page: {detected.CodePage}"); sb.AppendLine($"BOM Present: {HasBom(bytes)}"); return ToolResult.Ok(sb.ToString()); } private static async Task ConvertEncoding(string path, JsonElement args, AgentContext context) { var toName = args.TryGetProperty("to_encoding", out var te) ? te.GetString() ?? "utf-8" : "utf-8"; // 쓰기 권한 확인 var allowed = await context.CheckWritePermissionAsync("encoding_tool", path); if (!allowed) return ToolResult.Fail("파일 쓰기 권한이 거부되었습니다."); // 소스 인코딩 결정 Encoding fromEnc; if (args.TryGetProperty("from_encoding", out var fe) && !string.IsNullOrEmpty(fe.GetString())) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); fromEnc = Encoding.GetEncoding(fe.GetString()!); } else { var rawBytes = File.ReadAllBytes(path); fromEnc = DetectEncodingFromBytes(rawBytes); } Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var toEnc = Encoding.GetEncoding(toName); var content = File.ReadAllText(path, fromEnc); File.WriteAllText(path, content, toEnc); return ToolResult.Ok( $"변환 완료: {fromEnc.EncodingName} → {toEnc.EncodingName}\nFile: {path}", filePath: path); } private static ToolResult ListEncodings() { var sb = new StringBuilder(); sb.AppendLine("주요 인코딩 목록:"); sb.AppendLine(" utf-8 — UTF-8 (유니코드, 기본)"); sb.AppendLine(" utf-16 — UTF-16 LE"); sb.AppendLine(" utf-16BE — UTF-16 BE"); sb.AppendLine(" euc-kr — EUC-KR (한국어)"); sb.AppendLine(" ks_c_5601-1987 — 한글 완성형"); sb.AppendLine(" shift_jis — Shift-JIS (일본어)"); sb.AppendLine(" gb2312 — GB2312 (중국어 간체)"); sb.AppendLine(" iso-8859-1 — Latin-1 (서유럽)"); sb.AppendLine(" ascii — US-ASCII"); sb.AppendLine(" utf-32 — UTF-32"); return ToolResult.Ok(sb.ToString()); } private static Encoding DetectEncodingFromBytes(byte[] bytes) { // BOM 감지 if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) return Encoding.UTF8; if (bytes.Length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE) return Encoding.Unicode; // UTF-16 LE if (bytes.Length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF) return Encoding.BigEndianUnicode; // 간단한 UTF-8 유효성 검사 if (IsValidUtf8(bytes)) return Encoding.UTF8; // 한국어 환경 기본값 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); try { return Encoding.GetEncoding("euc-kr"); } catch { return Encoding.Default; } } private static bool IsValidUtf8(byte[] bytes) { var i = 0; var hasMultibyte = false; while (i < bytes.Length) { if (bytes[i] <= 0x7F) { i++; continue; } int extra; if ((bytes[i] & 0xE0) == 0xC0) extra = 1; else if ((bytes[i] & 0xF0) == 0xE0) extra = 2; else if ((bytes[i] & 0xF8) == 0xF0) extra = 3; else return false; if (i + extra >= bytes.Length) return false; for (var j = 1; j <= extra; j++) if ((bytes[i + j] & 0xC0) != 0x80) return false; hasMultibyte = true; i += extra + 1; } return hasMultibyte || bytes.Length < 100; // 순수 ASCII도 UTF-8으로 간주 } private static bool HasBom(byte[] bytes) { if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) return true; if (bytes.Length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE) return true; if (bytes.Length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF) return true; return false; } }