using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L20-4: Unix 파일 권한 계산기 핸들러. "perm" 프리픽스로 사용합니다. /// /// 예: perm → 주요 권한 목록 /// perm 755 → rwxr-xr-x 상세 설명 /// perm 644 → rw-r--r-- 상세 설명 /// perm rwxr-xr-x → 기호 → 숫자 변환 /// perm rw-r--r-- → 기호 → 숫자 변환 /// perm +x 644 → 실행 비트 추가 /// perm -x 755 → 실행 비트 제거 /// perm +w 444 → 쓰기 비트 추가 /// perm umask 022 → umask 적용 결과 /// perm common → 자주 쓰는 권한 목록 /// Enter → 값 복사. /// public class PermHandler : IActionHandler { public string? Prefix => "perm"; public PluginMetadata Metadata => new( "Perm", "Unix 파일 권한 계산기 — chmod 숫자↔기호·umask·권한 설명", "1.0", "AX"); private record CommonPerm(string Octal, string Symbol, string Description, string UseCase); private static readonly CommonPerm[] Common = [ new("777", "rwxrwxrwx", "모든 사용자 완전 권한", "임시 스크립트 (보안 주의)"), new("755", "rwxr-xr-x", "소유자 완전·그룹/기타 읽기+실행", "실행 파일, 디렉토리"), new("750", "rwxr-x---", "소유자 완전·그룹 읽기+실행·기타 없음", "그룹 공유 스크립트"), new("700", "rwx------", "소유자만 완전 권한", "개인 스크립트"), new("644", "rw-r--r--", "소유자 읽기+쓰기·그룹/기타 읽기만", "일반 파일"), new("640", "rw-r-----", "소유자 읽기+쓰기·그룹 읽기·기타 없음", "설정 파일"), new("600", "rw-------", "소유자만 읽기+쓰기", "개인 설정·SSH 키"), new("444", "r--r--r--", "모든 사용자 읽기만", "공유 읽기 전용"), new("400", "r--------", "소유자만 읽기", "SSL 인증서·개인 키"), new("666", "rw-rw-rw-", "모든 사용자 읽기+쓰기 (실행 없음)", "임시 파일"), new("664", "rw-rw-r--", "소유자+그룹 읽기+쓰기·기타 읽기", "그룹 협업 파일"), new("660", "rw-rw----", "소유자+그룹 읽기+쓰기·기타 없음", "그룹 협업 파일"), new("775", "rwxrwxr-x", "소유자+그룹 완전·기타 읽기+실행", "그룹 협업 디렉토리"), new("770", "rwxrwx---", "소유자+그룹 완전·기타 없음", "그룹 전용 디렉토리"), ]; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("Unix 파일 권한 계산기", "perm 755 / perm rwxr-xr-x / perm +x 644 / perm umask 022 / perm common", null, null, Symbol: "\uE8A5")); foreach (var p in Common.Take(6)) items.Add(new LauncherItem($"{p.Octal} {p.Symbol}", $"{p.Description} ({p.UseCase})", null, ("copy", p.Octal), Symbol: "\uE8A5")); return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); // common if (sub is "common" or "list" or "목록") { items.Add(new LauncherItem("자주 쓰는 파일 권한 목록", "", null, null, Symbol: "\uE8A5")); foreach (var p in Common) items.Add(new LauncherItem($"{p.Octal} {p.Symbol}", $"{p.Description} · {p.UseCase}", null, ("copy", p.Octal), Symbol: "\uE8A5")); return Task.FromResult>(items); } // umask if (sub == "umask" && parts.Length >= 2) { if (TryParseOctal(parts[1], out var umaskVal)) items.AddRange(BuildUmask(umaskVal)); else items.Add(ErrorItem("umask 값은 3자리 8진수입니다 (예: 022)")); return Task.FromResult>(items); } // +x / -x / +w / -w / +r / -r (비트 수정) if (sub.Length == 2 && sub[0] is '+' or '-' && sub[1] is 'r' or 'w' or 'x') { if (parts.Length >= 2 && TryParseOctal(parts[1], out var baseVal)) items.AddRange(BuildBitModify(sub[0] == '+', sub[1], baseVal)); else items.Add(ErrorItem($"예: perm {sub} 644")); return Task.FromResult>(items); } // 기호 표기 (rwxr-xr-x) if (sub.Length == 9 && sub.All(c => c is 'r' or 'w' or 'x' or '-')) { items.AddRange(BuildFromSymbol(sub)); return Task.FromResult>(items); } // 8진수 표기 (755, 644...) if (TryParseOctal(sub, out var octal)) { items.AddRange(BuildFromOctal(octal)); return Task.FromResult>(items); } items.Add(new LauncherItem($"인식할 수 없는 입력: '{sub}'", "perm 755 / perm rwxr-xr-x / perm +x 644 / perm umask 022", null, null, Symbol: "\uE783")); 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("Perm", "클립보드에 복사했습니다."); } catch { } } return Task.CompletedTask; } // ── 빌더 ──────────────────────────────────────────────────────────────── private static IEnumerable BuildFromOctal(int octal) { var sym = OctalToSymbol(octal); var desc = DescribeOctal(octal); var preset = Common.FirstOrDefault(p => p.Octal == octal.ToString("D3")); yield return new LauncherItem($"{octal:D3} → {sym}", preset?.Description ?? desc, null, ("copy", octal.ToString("D3")), Symbol: "\uE8A5"); yield return CopyItem("8진수", octal.ToString("D3")); yield return CopyItem("기호 표기", sym); yield return CopyItem("chmod 명령", $"chmod {octal:D3} <파일>"); var parts = OctalToParts(octal); yield return new LauncherItem("소유자 (Owner)", $"{OctetToSymbol(parts[0])} ({BitsDesc(parts[0])})", null, null, Symbol: "\uE8A5"); yield return new LauncherItem("그룹 (Group)", $"{OctetToSymbol(parts[1])} ({BitsDesc(parts[1])})", null, null, Symbol: "\uE8A5"); yield return new LauncherItem("기타 (Others)", $"{OctetToSymbol(parts[2])} ({BitsDesc(parts[2])})", null, null, Symbol: "\uE8A5"); if (preset != null) yield return new LauncherItem("용도", preset.UseCase, null, null, Symbol: "\uE8A5"); // 관련 권한 제안 yield return new LauncherItem("── 관련 권한 ──", "", null, null, Symbol: "\uE8A5"); var related = Common.Where(p => Math.Abs(int.Parse(p.Octal) - octal) <= 11) .Take(4).ToList(); foreach (var r in related) yield return new LauncherItem($"{r.Octal} {r.Symbol}", r.Description, null, ("copy", r.Octal), Symbol: "\uE8A5"); } private static IEnumerable BuildFromSymbol(string sym) { var octal = SymbolToOctal(sym); return BuildFromOctal(octal); } private static IEnumerable BuildUmask(int umask) { var fileDefault = 0666; var dirDefault = 0777; var fileResult = fileDefault & ~umask; var dirResult = dirDefault & ~umask; yield return new LauncherItem($"umask {umask:D3} → 파일: {fileResult:D3} 디렉토리: {dirResult:D3}", "umask 적용 결과", null, null, Symbol: "\uE8A5"); yield return CopyItem("umask 값", umask.ToString("D3")); yield return CopyItem("기본 파일 권한", fileResult.ToString("D3")); yield return CopyItem("기본 디렉토리 권한", dirResult.ToString("D3")); yield return CopyItem("파일 기호", OctalToSymbol(fileResult)); yield return CopyItem("디렉토리 기호", OctalToSymbol(dirResult)); yield return new LauncherItem("설명", $"umask {umask:D3}: 파일={OctalToSymbol(fileResult)}, 디렉토리={OctalToSymbol(dirResult)}", null, null, Symbol: "\uE8A5"); } private static IEnumerable BuildBitModify(bool add, char bit, int baseOctal) { var mask = bit switch { 'r' => 0444, 'w' => 0222, 'x' => 0111, _ => 0 }; var result = add ? (baseOctal | mask) : (baseOctal & ~mask); result &= 0777; var label = add ? $"+{bit}" : $"-{bit}"; yield return new LauncherItem($"{baseOctal:D3} {label} → {result:D3} ({OctalToSymbol(result)})", "Enter 복사", null, ("copy", result.ToString("D3")), Symbol: "\uE8A5"); yield return CopyItem("변경 전", baseOctal.ToString("D3")); yield return CopyItem("변경 후", result.ToString("D3")); yield return CopyItem("기호", OctalToSymbol(result)); yield return CopyItem("chmod", $"chmod {label} <파일> (또는 chmod {result:D3} <파일>)"); } // ── 변환 헬퍼 ──────────────────────────────────────────────────────────── private static bool TryParseOctal(string s, out int val) { val = 0; s = s.TrimStart('0'); if (string.IsNullOrEmpty(s)) { val = 0; return true; } if (s.Length > 4) return false; try { val = Convert.ToInt32(s, 8); return val <= 0777; } catch { return false; } } private static int[] OctalToParts(int octal) => [(octal >> 6) & 7, (octal >> 3) & 7, octal & 7]; private static string OctetToSymbol(int v) => $"{((v & 4) != 0 ? 'r' : '-')}{((v & 2) != 0 ? 'w' : '-')}{((v & 1) != 0 ? 'x' : '-')}"; private static string OctalToSymbol(int octal) { var p = OctalToParts(octal); return OctetToSymbol(p[0]) + OctetToSymbol(p[1]) + OctetToSymbol(p[2]); } private static int SymbolToOctal(string sym) { int TriBit(int off) => ((sym[off] == 'r' ? 4 : 0) | (sym[off + 1] == 'w' ? 2 : 0) | (sym[off + 2] == 'x' ? 1 : 0)); return (TriBit(0) << 6) | (TriBit(3) << 3) | TriBit(6); } private static string BitsDesc(int v) { var parts = new List(); if ((v & 4) != 0) parts.Add("읽기"); if ((v & 2) != 0) parts.Add("쓰기"); if ((v & 1) != 0) parts.Add("실행"); return parts.Count == 0 ? "없음" : string.Join("·", parts); } private static string DescribeOctal(int octal) { var p = OctalToParts(octal); return $"소유자:{BitsDesc(p[0])} 그룹:{BitsDesc(p[1])} 기타:{BitsDesc(p[2])}"; } private static LauncherItem CopyItem(string label, string value) => new(label, value, null, ("copy", value), Symbol: "\uE8A5"); private static LauncherItem ErrorItem(string msg) => new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783"); }