From dc3a0150f3ac51d813c6e961e23f032f8dc040f0 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 12 Oct 2026 23:10:13 +0000 Subject: [PATCH] Read --index and --words as decimal numbers (closes #75) Both flags were uint32 flags, which the flag library reads with Go's base-0 rules, so `-n 010` derived key 8 and `0x10` or `1_0` were accepted. They are now string flags read by options.Decimal, which takes decimal digits only: a leading zero stays decimal, and a base prefix, an underscore or a sign is refused with an error naming the flag, before the mnemonic is looked for. --help shows both as a number, and the README says the index is a decimal number. Model: opus-5-5 --- README.md | 4 ++- internal/cli/cli_test.go | 47 +++++++++++++++++++++++++++++++ internal/cli/mnemonic/mnemonic.go | 11 ++++---- internal/cli/options/options.go | 28 ++++++++++++++---- 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4372b8b..57b39bd 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,9 @@ m/83696968'/'/' to one tool. Each key type's number is spelled the way BIP-85 spells its own RSA application (`828365` is the ASCII codes of `R`, `S`, `A` written out): SSH is `838372` (`S` `S` `H`), age is `657169` (`A` `G` `E`). -- `n` is the key index: flag `--index` / `-n`, default `0`. +- `n` is the key index, a decimal number: flag `--index` / `-n`, default `0`. A + leading zero is still decimal (`-n 010` is index 10); a base prefix, an + underscore or a sign (`0x10`, `1_0`, `+1`) is refused. ## Giving it the mnemonic diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 0b29adc..d4645c2 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -2,6 +2,7 @@ package cli_test import ( "bytes" + "strconv" "strings" "testing" @@ -106,6 +107,36 @@ func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) { require.Empty(t, out) } +func TestAnIndexWithALeadingZeroIsDecimal(t *testing.T) { + t.Setenv(mnemonic.Variable, example()) + + line := run(t, "ssh", "pub", "-n", "010") + require.Equal(t, run(t, "ssh", "pub", "-n", "10"), line) + require.True(t, + strings.HasSuffix(strings.TrimSpace(line), " keyfunc/ssh/10"), + ) +} + +// The mnemonic command in these runs fails, so a refusal naming the +// flag shows that the number was refused before the mnemonic was +// looked for. +func TestAnIndexThatIsNotDecimalDigitsIsRefused(t *testing.T) { + t.Parallel() + + for _, index := range []string{"0x10", "1_0", "+1", "-1"} { + t.Run(index, func(t *testing.T) { + t.Parallel() + + out, err := execute(t, + "ssh", "pub", "-n", index, "--mnemonic-command", "exit 1", + ) + require.ErrorIs(t, err, strconv.ErrSyntax) + require.ErrorContains(t, err, "--index") + require.Empty(t, out) + }) + } +} + func TestTheMnemonicCommandIsUsed(t *testing.T) { t.Setenv(mnemonic.Variable, "") @@ -138,6 +169,22 @@ func TestALengthTheToolDoesNotOfferIsRefused(t *testing.T) { require.Empty(t, out) } +func TestTheWordCountIsDecimal(t *testing.T) { + t.Setenv(mnemonic.Variable, example()) + + words := strings.Fields(run(t, "mnemonic", "--words", "012")) + require.Len(t, words, twelve) + + // As for the index, the failing mnemonic command shows that the + // number was refused first. + out, err := execute(t, + "mnemonic", "--words", "0x18", "--mnemonic-command", "exit 1", + ) + require.ErrorIs(t, err, strconv.ErrSyntax) + require.ErrorContains(t, err, "--words") + require.Empty(t, out) +} + // run executes the tool with the given arguments and returns what it // wrote to standard output. func run(t *testing.T, args ...string) string { diff --git a/internal/cli/mnemonic/mnemonic.go b/internal/cli/mnemonic/mnemonic.go index 8c33332..a2e0f5b 100644 --- a/internal/cli/mnemonic/mnemonic.go +++ b/internal/cli/mnemonic/mnemonic.go @@ -3,6 +3,7 @@ package mnemonic import ( "fmt" + "strconv" "github.com/spf13/cobra" "sneak.berlin/go/keyfunc/internal/childmnemonic" @@ -31,9 +32,9 @@ func Command() *cobra.Command { }, } - cmd.Flags().Uint32( - "words", childmnemonic.DefaultWords, - "how long the child mnemonic is: 12, 18 or 24 words", + cmd.Flags().String( + "words", strconv.Itoa(childmnemonic.DefaultWords), + "the `number` of words in the child mnemonic: 12, 18 or 24", ) return cmd @@ -46,9 +47,9 @@ func derived(cmd *cobra.Command) (string, error) { return "", err } - words, err := cmd.Flags().GetUint32("words") + words, err := options.Decimal(cmd, "words") if err != nil { - return "", fmt.Errorf("reading the word count: %w", err) + return "", err } parent, err := options.Mnemonic(cmd) diff --git a/internal/cli/options/options.go b/internal/cli/options/options.go index 5d454ec..5a240f4 100644 --- a/internal/cli/options/options.go +++ b/internal/cli/options/options.go @@ -4,6 +4,7 @@ package options import ( "fmt" + "strconv" "github.com/spf13/cobra" "sneak.berlin/go/keyfunc/internal/mnemonic" @@ -16,20 +17,35 @@ func Add(cmd *cobra.Command) { "mnemonic-command", "", "shell command whose output is the mnemonic", ) - cmd.PersistentFlags().Uint32P( - "index", "n", 0, - "which key to derive", + cmd.PersistentFlags().StringP( + "index", "n", "0", + "which key to derive, as a decimal `number`", ) } // Index returns the key index the user asked for. func Index(cmd *cobra.Command) (uint32, error) { - index, err := cmd.Flags().GetUint32("index") + return Decimal(cmd, "index") +} + +// Decimal reads the named string flag as a number written in decimal +// digits only: a leading zero is still decimal, and a base prefix, an +// underscore or a sign is refused. The back-quoted word in the usage +// of such a flag is what --help shows as its type. +func Decimal(cmd *cobra.Command, name string) (uint32, error) { + text, err := cmd.Flags().GetString(name) if err != nil { - return 0, fmt.Errorf("reading the index: %w", err) + return 0, fmt.Errorf("reading --%s: %w", name, err) } - return index, nil + number, err := strconv.ParseUint(text, 10, 32) + if err != nil { + return 0, fmt.Errorf( + "reading --%s as a decimal number: %w", name, err, + ) + } + + return uint32(number), nil } // Mnemonic returns the mnemonic, from the first source that has one.