From 3d8d1e5a669b9f76cd256ec432537ba83f031a58 Mon Sep 17 00:00:00 2001 From: Thayol Date: Sun, 13 Sep 2026 10:34:28 +0200 Subject: [PATCH] Fix CLI token set command --- internal/config/config_test.go | 44 ++++++++++++++++++++++++++++++++++ internal/config/flags.go | 15 ++++++++++-- token.go | 13 +++++++--- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 74860dd..dbdf715 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -112,6 +112,50 @@ func TestFormatDurationSpelling(t *testing.T) { } } +// A word the parser does not expect has to be refused, not ignored: the flag +// package stops at the first non-flag argument, so a command that accepted one +// would silently drop every option after it - including the one naming the +// file it writes. +func TestParseRefusesStrayArguments(t *testing.T) { + newSet := func() (*Set, *string, *bool) { + var listen string + var flagged bool + s := NewSet("test", "UNCENSORED_SEND_TEST_") + s.String(&listen, "listen", "l", "default", "ADDR", "listen address") + s.Bool(&flagged, "vanity", "", false, "a boolean") + return s, &listen, &flagged + } + + for _, args := range [][]string{ + {"--vanity", "false"}, // the value a boolean never takes + {"stray"}, // a word on its own + {"--vanity", "false", "-l", "here"}, // options after the stray word + } { + s, listen, _ := newSet() + err := s.Parse(args) + if err == nil { + t.Errorf("Parse(%q) was accepted; listen = %q", args, *listen) + continue + } + if !strings.Contains(err.Error(), "unexpected argument") { + t.Errorf("Parse(%q) failed with %v, which does not name the problem", args, err) + } + } + + // The forms that are actually correct still parse. + for _, args := range [][]string{ + {"--vanity=false"}, + {"--vanity", "-l", "here"}, + {"-l", "here"}, + {}, + } { + s, _, _ := newSet() + if err := s.Parse(args); err != nil { + t.Errorf("Parse(%q) = %v, want it accepted", args, err) + } + } +} + // The convention is: one hyphen for a letter, two for a word. It is enforced // here because the stdlib flag package treats both forms as the same. func TestHyphenConvention(t *testing.T) { diff --git a/internal/config/flags.go b/internal/config/flags.go index 0dfbdc3..80cb0a8 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -42,7 +42,6 @@ func NewSet(name, envPrefix string) *Set { return s } -func (s *Set) Args() []string { return s.fs.Args() } func (s *Set) SetOutput(w io.Writer) { s.fs.SetOutput(w) } func (s *Set) register(sp *spec) { s.specs = append(s.specs, sp) } func (s *Set) note(long, short string) { @@ -208,7 +207,19 @@ func (s *Set) Parse(args []string) error { if err := s.checkConvention(args); err != nil { return err } - return s.fs.Parse(args) + if err := s.fs.Parse(args); err != nil { + return err + } + // A stray word is never harmless: the flag package stops parsing at the + // first non-flag argument, so every option after it is dropped in silence. + // Writing a boolean as "--vanity false" is the way this bites - it sets + // the flag to true and then discards the --data that says which file to + // write, leaving a cheerful message about the opposite of what was meant. + if extra := s.fs.Args(); len(extra) > 0 { + return fmt.Errorf("unexpected argument %q: an option's value attaches with %q, as in --name=value, and any positional argument comes first", + extra[0], "=") + } + return nil } // Changed reports whether an option was given on the command line. diff --git a/token.go b/token.go index d011559..b118def 100644 --- a/token.go +++ b/token.go @@ -28,7 +28,14 @@ Usage: A token grants its own size and lifetime limits. Any limit left unset is inherited from the running server's defaults, so a token with no options -behaves exactly like the anonymous tier but may claim vanity names. +behaves exactly like the anonymous tier. + +"set" changes only what you name on the command line; anything you leave out +keeps its current value. Give a limit an empty value to go back to inheriting +the server's default, and turn a flag off with "=false": + + uncensored-send token set friend --max-size= inherit the default again + uncensored-send token set friend --vanity=false revoke custom names Options: ` @@ -144,8 +151,8 @@ func (o *tokenOptions) register() *config.Set { fs.String(&o.defExpiry, "default-expiry", "", "", "DURATION", "lifetime applied when this token does not ask for one") fs.String(&o.chosen, "token", "t", "", "VALUE", "use this token instead of a generated one; \"-\" reads it from standard input") - fs.Bool(&o.vanity, "vanity", "", false, "allow this token to claim custom names") - fs.Bool(&o.admin, "admin", "", false, "allow this token to delete anyone's files") + fs.Bool(&o.vanity, "vanity", "", false, "allow this token to claim custom names; --vanity=false revokes it") + fs.Bool(&o.admin, "admin", "", false, "allow this token to delete anyone's files; --admin=false revokes it") return fs }