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");
}