-
-
Notifications
You must be signed in to change notification settings - Fork 27
feat: add unixperms command to explain Unix file permission modes #206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| // permBit describes one permission bit. | ||
| type permBit struct { | ||
| symbol string | ||
| name string | ||
| value int | ||
| } | ||
|
|
||
| var permBits = []permBit{ | ||
| {"r", "owner read", 0o400}, | ||
| {"w", "owner write", 0o200}, | ||
| {"x", "owner execute", 0o100}, | ||
| {"r", "group read", 0o040}, | ||
| {"w", "group write", 0o020}, | ||
| {"x", "group execute", 0o010}, | ||
| {"r", "other read", 0o004}, | ||
| {"w", "other write", 0o002}, | ||
| {"x", "other execute", 0o001}, | ||
| } | ||
|
|
||
| // octalToSymbolic converts an octal permission mode to a 9-char symbolic string (e.g. "rwxr-xr-x"). | ||
| func octalToSymbolic(mode int) string { | ||
| var sb strings.Builder | ||
| for _, bit := range permBits { | ||
| if mode&bit.value != 0 { | ||
| sb.WriteString(bit.symbol) | ||
| } else { | ||
| sb.WriteString("-") | ||
| } | ||
| } | ||
| return sb.String() | ||
| } | ||
|
|
||
| // symbolicToOctal parses a 9-char symbolic string and returns the octal mode. | ||
| func symbolicToOctal(sym string) (int, error) { | ||
| if len(sym) != 9 { | ||
| return 0, errors.New("symbolic notation must be exactly 9 characters (e.g. rwxr-xr-x)") | ||
| } | ||
| mode := 0 | ||
| for i, bit := range permBits { | ||
| ch := string(sym[i]) | ||
| if ch == bit.symbol { | ||
| mode |= bit.value | ||
| } else if ch != "-" { | ||
| return 0, fmt.Errorf("invalid character %q at position %d (expected %q or \"-\")", ch, i+1, bit.symbol) | ||
| } | ||
| } | ||
| return mode, nil | ||
| } | ||
|
|
||
| // explainMode prints a human-readable breakdown of the permission mode. | ||
| func explainMode(mode int) { | ||
| symbolic := octalToSymbolic(mode) | ||
| fmt.Printf("Octal: %04o\n", mode) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please see how other cmd/'s have been implemented. To render output, you're using |
||
| fmt.Printf("Symbolic: %s\n\n", symbolic) | ||
|
|
||
| sections := []struct { | ||
| label string | ||
| bits []permBit | ||
| start int | ||
| }{ | ||
| {"Owner", permBits[0:3], 0}, | ||
| {"Group", permBits[3:6], 3}, | ||
| {"Other", permBits[6:9], 6}, | ||
| } | ||
|
|
||
| for _, sec := range sections { | ||
| var granted []string | ||
| for _, bit := range sec.bits { | ||
| if mode&bit.value != 0 { | ||
| granted = append(granted, bit.symbol) | ||
| } else { | ||
| granted = append(granted, "-") | ||
| } | ||
| } | ||
| fmt.Printf("%s: %s (%s)\n", sec.label, strings.Join(granted, ""), describeSection(mode, sec.start)) | ||
| } | ||
| } | ||
|
|
||
| func describeSection(mode, start int) string { | ||
| bits := permBits[start : start+3] | ||
| var parts []string | ||
| for _, bit := range bits { | ||
| if mode&bit.value != 0 { | ||
| switch bit.symbol { | ||
| case "r": | ||
| parts = append(parts, "read") | ||
| case "w": | ||
| parts = append(parts, "write") | ||
| case "x": | ||
| parts = append(parts, "execute") | ||
| } | ||
| } | ||
| } | ||
| if len(parts) == 0 { | ||
| return "no permissions" | ||
| } | ||
| return strings.Join(parts, ", ") | ||
| } | ||
|
|
||
| var unixpermsCmd = &cobra.Command{ | ||
| Use: "unixperms <mode>", | ||
| Short: "Explain Unix file permission modes", | ||
| Long: `Explain Unix file permission modes. | ||
|
|
||
| Accepts either octal notation (e.g. 755, 0644) or 9-character symbolic | ||
| notation (e.g. rwxr-xr-x). Prints a human-readable breakdown of each | ||
| permission bit for owner, group, and other.`, | ||
| Example: ` # Explain octal mode 755 | ||
| devtui unixperms 755 | ||
|
|
||
| # Explain octal mode with leading zero | ||
| devtui unixperms 0644 | ||
|
|
||
| # Explain symbolic notation | ||
| devtui unixperms rwxr-xr-x`, | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| input := strings.TrimSpace(args[0]) | ||
|
|
||
| var mode int | ||
|
|
||
| // Detect whether input is octal (all digits, optional leading 0) or symbolic. | ||
| if strings.ContainsAny(input, "rwx-") || len(input) == 9 { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's quite an assumption about symbolic value. How would you handle following values '--S--S--T', 'u+rwx,g+w', 'u-rwx,g-w', 'u=rwx,g=w,o='? |
||
| // Symbolic notation | ||
| m, err := symbolicToOctal(input) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe instead of reimplementing octal to symbol conversion, rely on existing library that is battle tested? as example: |
||
| if err != nil { | ||
| return err | ||
| } | ||
| mode = m | ||
| } else { | ||
| // Octal notation | ||
| stripped := strings.TrimPrefix(input, "0") | ||
| if stripped == "" { | ||
| stripped = "0" | ||
| } | ||
| m, err := strconv.ParseInt(stripped, 8, 64) | ||
| if err != nil { | ||
| return fmt.Errorf("invalid octal mode %q: %v", input, err) | ||
| } | ||
| if m < 0 || m > 0o777 { | ||
| return fmt.Errorf("mode %04o out of range (must be 000-777)", m) | ||
| } | ||
| mode = int(m) | ||
| } | ||
|
|
||
| explainMode(mode) | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| func init() { | ||
| rootCmd.AddCommand(unixpermsCmd) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestOctalToSymbolic(t *testing.T) { | ||
| cases := []struct { | ||
| octal int | ||
| symbolic string | ||
| }{ | ||
| {0o755, "rwxr-xr-x"}, | ||
| {0o644, "rw-r--r--"}, | ||
| {0o600, "rw-------"}, | ||
| {0o777, "rwxrwxrwx"}, | ||
| {0o000, "---------"}, | ||
| {0o444, "r--r--r--"}, | ||
| } | ||
| for _, tc := range cases { | ||
| got := octalToSymbolic(tc.octal) | ||
| if got != tc.symbolic { | ||
| t.Errorf("octalToSymbolic(%04o) = %q, want %q", tc.octal, got, tc.symbolic) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestSymbolicToOctal(t *testing.T) { | ||
| cases := []struct { | ||
| symbolic string | ||
| octal int | ||
| }{ | ||
| {"rwxr-xr-x", 0o755}, | ||
| {"rw-r--r--", 0o644}, | ||
| {"rw-------", 0o600}, | ||
| {"rwxrwxrwx", 0o777}, | ||
| {"---------", 0o000}, | ||
| } | ||
| for _, tc := range cases { | ||
| got, err := symbolicToOctal(tc.symbolic) | ||
| if err != nil { | ||
| t.Errorf("symbolicToOctal(%q) unexpected error: %v", tc.symbolic, err) | ||
| continue | ||
| } | ||
| if got != tc.octal { | ||
| t.Errorf("symbolicToOctal(%q) = %04o, want %04o", tc.symbolic, got, tc.octal) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestSymbolicToOctalErrors(t *testing.T) { | ||
| bad := []string{"rwx", "rwxr-xr-xx", "abc123xyz"} | ||
| for _, s := range bad { | ||
| if _, err := symbolicToOctal(s); err == nil { | ||
| t.Errorf("symbolicToOctal(%q) expected error, got nil", s) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestRoundTrip(t *testing.T) { | ||
| for mode := 0; mode <= 0o777; mode++ { | ||
| sym := octalToSymbolic(mode) | ||
| got, err := symbolicToOctal(sym) | ||
| if err != nil { | ||
| t.Fatalf("round-trip failed for %04o: %v", mode, err) | ||
| } | ||
| if got != mode { | ||
| t.Errorf("round-trip mismatch for %04o: symbolic=%q -> %04o", mode, sym, got) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,13 +24,18 @@ const ( | |
| uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
| numbers = "0123456789" | ||
| special = "!@#$%^&*()_+-=[]{}|;:,.<>?" | ||
|
|
||
| charsetLowercase = "Lowercase (a-z)" | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did you extract it into separate variables? why not add this value to characterSetOptions? |
||
| charsetUppercase = "Uppercase (A-Z)" | ||
| charsetNumbers = "Numbers (0-9)" | ||
| charsetSpecial = "Special (!@#$%^&*)" | ||
| ) | ||
|
|
||
| var characterSetOptions = []huh.Option[string]{ | ||
| {Key: "Lowercase (a-z)", Value: "Lowercase (a-z)"}, | ||
| {Key: "Uppercase (A-Z)", Value: "Uppercase (A-Z)"}, | ||
| {Key: "Numbers (0-9)", Value: "Numbers (0-9)"}, | ||
| {Key: "Special (!@#$%^&*)", Value: "Special (!@#$%^&*)"}, | ||
| {Key: charsetLowercase, Value: charsetLowercase}, | ||
| {Key: charsetUppercase, Value: charsetUppercase}, | ||
| {Key: charsetNumbers, Value: charsetNumbers}, | ||
| {Key: charsetSpecial, Value: charsetSpecial}, | ||
| } | ||
|
|
||
| func main() { | ||
|
|
@@ -120,13 +125,13 @@ func generateSecurePassword(config *PasswordConfig) (string, error) { | |
|
|
||
| for _, set := range config.CharacterSets { | ||
| switch set { | ||
| case "Lowercase (a-z)": | ||
| case charsetLowercase: | ||
| charPool.WriteString(lowercase) | ||
| case "Uppercase (A-Z)": | ||
| case charsetUppercase: | ||
| charPool.WriteString(uppercase) | ||
| case "Numbers (0-9)": | ||
| case charsetNumbers: | ||
| charPool.WriteString(numbers) | ||
| case "Special (!@#$%^&*)": | ||
| case charsetSpecial: | ||
| charPool.WriteString(special) | ||
| } | ||
| } | ||
|
|
@@ -171,13 +176,13 @@ func ensureAllSetsIncluded(password []byte, config *PasswordConfig) error { | |
| for i, set := range config.CharacterSets { | ||
| var chars string | ||
| switch set { | ||
| case "Lowercase (a-z)": | ||
| case charsetLowercase: | ||
| chars = lowercase | ||
| case "Uppercase (A-Z)": | ||
| case charsetUppercase: | ||
| chars = uppercase | ||
| case "Numbers (0-9)": | ||
| case charsetNumbers: | ||
| chars = numbers | ||
| case "Special (!@#$%^&*)": | ||
| case charsetSpecial: | ||
| chars = special | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name is never used.