Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 37 additions & 21 deletions handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package handler
import (
"fmt"
"strings"
"unicode/utf8"

"github.com/joseluisq/cline/app"
"github.com/joseluisq/cline/flag"
Expand All @@ -29,6 +30,10 @@ func New(ap *app.App) *Handler {
// Note that the first argument is always skipped.
func (h *Handler) Run(vArgs []string) error {
// Commands and flags validation
var vArgsLen = len(vArgs)
if vArgsLen > maxArgsCount {
return fmt.Errorf("error: number of arguments exceeds the limit of %d", maxArgsCount)
}

// 1. Check application global flags
vflags, err := helpers.ValidateFlagsAndInit(h.ap.Flags)
Expand All @@ -44,9 +49,8 @@ func (h *Handler) Run(vArgs []string) error {
}
h.ap.Commands = vcmds

// Optimization: Create maps for faster flag lookups
appFlagMap := helpers.BuildFlagMap(h.ap.Flags)
cmdFlagMaps := make(map[string]map[string]helpers.FlagInfo, len(h.ap.Commands))
var appFlagMap = helpers.BuildFlagMap(h.ap.Flags)
var cmdFlagMaps = make(map[string]map[string]helpers.FlagInfo, len(h.ap.Commands))
for _, cmd := range h.ap.Commands {
cmdFlagMaps[cmd.Name] = helpers.BuildFlagMap(cmd.Flags)
}
Expand All @@ -59,11 +63,6 @@ func (h *Handler) Run(vArgs []string) error {
var hasCmd = false
var hasHelp = false
var hasVersion = false
var vArgsLen = len(vArgs)

if vArgsLen > maxArgsCount {
return fmt.Errorf("error: number of arguments exceeds the limit of %d", maxArgsCount)
}

for idx := 1; idx < vArgsLen; idx++ {
arg := strings.TrimSpace(vArgs[idx])
Expand All @@ -72,6 +71,10 @@ func (h *Handler) Run(vArgs []string) error {
return fmt.Errorf("error: argument exceeds maximum length of %d characters", maxArgLen)
}

if !utf8.ValidString(arg) {
return fmt.Errorf("error: argument contains invalid UTF-8 characters")
}

// Check if the previous flag was expecting a value but didn't get one
if lastFlag != nil {
// Check if the flag type requires a value (i.e., it's not a bool)
Expand Down Expand Up @@ -99,7 +102,7 @@ func (h *Handler) Run(vArgs []string) error {
// it's an error.
if isUnassignedValueFlag && strings.HasPrefix(arg, "-") {
// The previous flag is missing its required value.
return fmt.Errorf("error: flag \"--%s\" requires a value", name)
return fmt.Errorf("error: flag '--%s' requires a value", name)
}
}

Expand All @@ -108,7 +111,7 @@ func (h *Handler) Run(vArgs []string) error {
if idx+1 < vArgsLen {
tailArgs = append(tailArgs, vArgs[idx+1:]...)
}
break // Stop processing further arguments as flags
break
}

if len(tailArgs) > 0 {
Expand All @@ -126,20 +129,33 @@ func (h *Handler) Run(vArgs []string) error {
flagKey = arg[2:]
}

// Skip unsupported flags
if flagKey == "" || strings.HasPrefix(flagKey, "-") {
tailArgs = append(tailArgs, arg)
continue
if err := helpers.IsValidToken(flagKey, "flag"); err != nil {
return err
}

// Process special flags (help and version)
switch flagKey {
case "help", "h":
case "help":
hasHelp = true
case "version", "v":
case "h":
if isAlias {
hasHelp = true
}
case "version":
hasVersion = true
if !hasCmd {
hasVersion = true
}
case "v":
if !hasCmd && isAlias {
hasVersion = true
}
}

// Skip unsupported flags
if flagKey == "" || strings.HasPrefix(flagKey, "-") {
tailArgs = append(tailArgs, arg)
continue
}
if hasHelp || hasVersion {
break
Expand All @@ -154,7 +170,7 @@ func (h *Handler) Run(vArgs []string) error {

flagInfo, ok := flagMap[flagKey]
if !ok {
return fmt.Errorf("error: unknown argument: %s", arg)
return fmt.Errorf("error: unknown flag '%s' argument", arg)
}
lastFlag = flagInfo.Flag
lastFlagIndex = flagInfo.Index
Expand Down Expand Up @@ -273,7 +289,7 @@ func (h *Handler) Run(vArgs []string) error {
}
continue
} else {
return fmt.Errorf("error: invalid integer value for flag --%s", fl.Name)
return fmt.Errorf("error: invalid integer value for flag '--%s'", fl.Name)
}
}
case flag.FlagString:
Expand Down Expand Up @@ -327,15 +343,15 @@ func (h *Handler) Run(vArgs []string) error {
switch v := lastFlag.(type) {
case flag.FlagInt:
if !v.FlagAssigned {
return fmt.Errorf("error: flag \"--%s\" requires a value", v.Name)
return fmt.Errorf("error: flag '--%s' requires a value", v.Name)
}
case flag.FlagString:
if !v.FlagAssigned {
return fmt.Errorf("error: flag \"--%s\" requires a value", v.Name)
return fmt.Errorf("error: flag '--%s' requires a value", v.Name)
}
case flag.FlagStringSlice:
if !v.FlagAssigned {
return fmt.Errorf("error: flag \"--%s\" requires a value", v.Name)
return fmt.Errorf("error: flag '--%s' requires a value", v.Name)
}
}
}
Expand Down
124 changes: 107 additions & 17 deletions handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestHandler_Run(t *testing.T) {
name: "should return error for unknown flag",
ap: &app.App{Flags: []flag.Flag{}},
vargs: []string{"app", "--unknown"},
wantErr: errors.New("error: unknown argument: --unknown"),
wantErr: errors.New("error: unknown flag '--unknown' argument"),
},
{
name: "should treat unknown command as tail argument",
Expand Down Expand Up @@ -64,7 +64,7 @@ func TestHandler_Run(t *testing.T) {
},
},
vargs: []string{"app", "--num", "notanint"},
wantErr: errors.New("error: invalid integer value for flag --num"),
wantErr: errors.New("error: invalid integer value for flag '--num'"),
},
{
name: "should parse valid int flag value",
Expand Down Expand Up @@ -215,7 +215,8 @@ func TestHandler_Run(t *testing.T) {
return nil
},
},
vargs: []string{"app", "---"},
vargs: []string{"app", "---"},
wantErr: fmt.Errorf("error: flag '-' contains invalid characters"),
},
{
name: "should handle path traversal attempt as a string",
Expand All @@ -227,19 +228,32 @@ func TestHandler_Run(t *testing.T) {
vargs: []string{"app", "--file", "../../../etc/passwd"},
},
{
name: "should handle non-UTF-8 characters in flag name",
name: "should return error when non-UTF-8 characters in flag name",
ap: &app.App{},
vargs: []string{"app", "--\xff\xfe\xfd"},
wantErr: fmt.Errorf("error: unknown argument: --\xff\xfe\xfd"),
wantErr: fmt.Errorf("error: argument contains invalid UTF-8 characters"),
},
{
name: "should handle non-UTF-8 characters in flag value",
name: "should return error when non-ASCII characters in flag name",
ap: &app.App{},
vargs: []string{"app", "--ñame"},
wantErr: fmt.Errorf("error: flag 'ñame' contains invalid characters"),
},
{
name: "should return error when non-ASCII characters in flag name with single dash",
ap: &app.App{},
vargs: []string{"app", "-–file"},
wantErr: fmt.Errorf("error: flag '–file' contains invalid characters"),
},
{
name: "should return error when non-UTF-8 characters in flag value",
ap: &app.App{
Flags: []flag.Flag{
flag.FlagString{Name: "input"},
},
},
vargs: []string{"app", "--input", "value-\xff\xfe\xfd"},
vargs: []string{"app", "--input", "value-\xff\xfe\xfd"},
wantErr: fmt.Errorf("error: argument contains invalid UTF-8 characters"),
},
{
name: "should handle null byte in argument",
Expand All @@ -251,15 +265,16 @@ func TestHandler_Run(t *testing.T) {
vargs: []string{"app", "--input", "some\x00value"},
},
{
name: "should handle invalid UTF-8 sequence as a tail argument",
name: "should return error when invalid UTF-8 sequence as a tail argument",
ap: &app.App{
Handler: func(ctx *app.AppContext) error {
assert.Equal(t, 1, len(ctx.TailArgs()), "should have one tail argument")
assert.Equal(t, "arg-\xff\xfe\xfd", ctx.TailArgs()[0])
return nil
},
},
vargs: []string{"app", "arg-\xff\xfe\xfd"},
vargs: []string{"app", "arg-\xff\xfe\xfd"},
wantErr: fmt.Errorf("error: argument contains invalid UTF-8 characters"),
},
{
name: "should overwrite string slice flag on multiple assignments",
Expand Down Expand Up @@ -298,7 +313,7 @@ func TestHandler_Run(t *testing.T) {
},
},
vargs: []string{"app", "--input", "--verbose"},
wantErr: fmt.Errorf("error: flag \"--input\" requires a value"),
wantErr: fmt.Errorf("error: flag '--input' requires a value"),
},
{
name: "should return error if string flag is last argument",
Expand All @@ -308,7 +323,7 @@ func TestHandler_Run(t *testing.T) {
},
},
vargs: []string{"app", "--input"},
wantErr: fmt.Errorf("error: flag \"--input\" requires a value"),
wantErr: fmt.Errorf("error: flag '--input' requires a value"),
},
{
name: "should return error if int flag is last argument",
Expand All @@ -318,7 +333,7 @@ func TestHandler_Run(t *testing.T) {
},
},
vargs: []string{"app", "--num"},
wantErr: fmt.Errorf("error: flag \"--num\" requires a value"),
wantErr: fmt.Errorf("error: flag '--num' requires a value"),
},
{
name: "should return error if string slice flag is last argument",
Expand All @@ -328,7 +343,54 @@ func TestHandler_Run(t *testing.T) {
},
},
vargs: []string{"app", "--items"},
wantErr: fmt.Errorf("error: flag \"--items\" requires a value"),
wantErr: fmt.Errorf("error: flag '--items' requires a value"),
},
{
name: "should return error for inconsistent special flag version alias",
ap: &app.App{
Flags: []flag.Flag{
flag.FlagStringSlice{Name: "items"},
},
},
vargs: []string{"app", "--v"},
wantErr: fmt.Errorf("error: unknown flag '--v' argument"),
},
{
name: "should execute when consistent special flag version alias",
ap: &app.App{
Flags: []flag.Flag{
flag.FlagStringSlice{Name: "items"},
},
},
vargs: []string{"app", "-v"},
},
{
name: "should return error for inconsistent special flag help alias",
ap: &app.App{
Flags: []flag.Flag{
flag.FlagStringSlice{Name: "items"},
},
},
vargs: []string{"app", "--h"},
wantErr: fmt.Errorf("error: unknown flag '--h' argument"),
},
{
name: "should execute when consistent special flag help alias",
ap: &app.App{
Flags: []flag.Flag{
flag.FlagStringSlice{Name: "items"},
},
},
vargs: []string{"app", "-h"},
},
{
name: "should skip when unsupported argument is provided",
ap: &app.App{
Flags: []flag.Flag{
flag.FlagStringSlice{Name: "items"},
},
},
vargs: []string{"app", "-"},
},
{
name: "should treat subsequent value as tail arg if flag already assigned",
Expand Down Expand Up @@ -382,6 +444,24 @@ func TestHandler_Run(t *testing.T) {
},
vargs: []string{"app", "run", "--local"},
},
{
name: "should return error for command with invalid ASCII characters",
ap: &app.App{
Flags: []flag.Flag{
flag.FlagBool{Name: "global"},
},
Commands: []app.Cmd{
{
Name: "ràn",
Flags: []flag.Flag{
flag.FlagBool{Name: "local"},
},
},
},
},
vargs: []string{"app", "run", "--local"},
wantErr: fmt.Errorf("error: command 'ràn' contains invalid characters"),
},
{
name: "should parse command-specific string flag when command is present",
ap: &app.App{
Expand Down Expand Up @@ -505,15 +585,25 @@ func TestHandler_Run(t *testing.T) {
},
vargs: []string{"app", "--verbose", "true", "extra-arg"},
},
{
name: "should return error for invalid flag",
ap: &app.App{
Flags: []flag.Flag{
flag.FlagStringSlice{Name: "itéms", Aliases: []string{"i"}},
},
},
vargs: []string{"app", "-i"},
wantErr: fmt.Errorf("error: flag 'itéms' contains invalid characters"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := New(tt.ap).Run(tt.vargs); tt.wantErr != nil {
assert.Error(t, err, "Expected an error but got none")
assert.Equal(t, err, tt.wantErr, "Error message does not match the expected one")
if actualErr := New(tt.ap).Run(tt.vargs); tt.wantErr != nil {
assert.Error(t, actualErr, "Expected an error but got none")
assert.Equal(t, actualErr, tt.wantErr, "Error message does not match the expected one")
} else {
assert.NoError(t, err, "Expected no error but got one")
assert.NoError(t, actualErr, "Expected no error but got one")
}
})
}
Expand Down
Loading
Loading