2 Commits
Author SHA1 Message Date
clawbot efca0ebb39 Read --index and --words as decimal numbers (closes #75)
check / check (push) Waiting to run
Both flags were uint32 flags, which the flag library reads with base-0 rules, so `-n 010` derived key 8 and `0x10` or `1_0` were accepted. Both now hold options.Decimal, a flag value whose Set parses with strconv.ParseUint(text, 10, 32): a leading zero stays decimal, and a base prefix, an underscore, a sign or an empty value is refused with an error naming the flag while the flags are parsed, before any command runs. Its type is still named uint32, so --help is unchanged and GetUint32 still reads it back. The README says the index is a decimal number.

Model: opus-5-5
2026-10-13 02:29:25 +02:00
clawbot 45463fa35b Require secret at the commit that drops go-bip39 (closes #70)
check / check (push) Successful in 5m8s
sneak/secret now carries its own copy of go-bip39
(sneak/secret#130), so requiring it at that
commit, ef0ae90, lets go mod tidy drop go-bip39 from go.sum. Earlier,
secret renamed its module to sneak.berlin/go/secret
(sneak/secret#43), so the requirement, the
two imports of its bip85 package and the README sentence naming that
package move to the new path. The bip85 functions keyfunc calls are
unchanged, and no other requirement moves.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
2026-10-07 13:43:37 +02:00
4 changed files with 106 additions and 5 deletions
+3 -1
View File
@@ -100,7 +100,9 @@ m/83696968'/<app>'/<n>'
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
+67
View File
@@ -2,6 +2,7 @@ package cli_test
import (
"bytes"
"strconv"
"strings"
"testing"
@@ -106,6 +107,56 @@ 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)
})
}
}
// Neither a command group nor --version reads the index, so these
// show that it is refused while the flags are parsed.
func TestAnIndexNotInDecimalIsRefusedBeforeAnyCommandRuns(t *testing.T) {
t.Parallel()
for _, args := range [][]string{
{"ssh", "-n", "0x10"},
{"-n", "0x10", "--version"},
} {
t.Run(strings.Join(args, " "), func(t *testing.T) {
t.Parallel()
out, err := execute(t, args...)
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 +189,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 {
+3 -2
View File
@@ -31,8 +31,9 @@ func Command() *cobra.Command {
},
}
cmd.Flags().Uint32(
"words", childmnemonic.DefaultWords,
words := options.Decimal(childmnemonic.DefaultWords)
cmd.Flags().Var(
&words, "words",
"how long the child mnemonic is: 12, 18 or 24 words",
)
+33 -2
View File
@@ -4,6 +4,7 @@ package options
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"sneak.berlin/go/keyfunc/internal/mnemonic"
@@ -16,8 +17,8 @@ func Add(cmd *cobra.Command) {
"mnemonic-command", "",
"shell command whose output is the mnemonic",
)
cmd.PersistentFlags().Uint32P(
"index", "n", 0,
cmd.PersistentFlags().VarP(
new(Decimal), "index", "n",
"which key to derive",
)
}
@@ -41,3 +42,33 @@ func Mnemonic(cmd *cobra.Command) (string, error) {
return mnemonic.Read(cmd.Context(), command)
}
// Decimal is a flag value for a number written in decimal digits only.
// The flag library's own uint32 flags read 010 as octal 8 and also
// take 0x10 or 1_0.
type Decimal uint32
// String returns the number in decimal digits.
func (d *Decimal) String() string {
return strconv.FormatUint(uint64(*d), 10)
}
// Set reads text as a decimal number that fits in 32 bits: a leading
// zero is still decimal, and a base prefix, an underscore or a sign is
// refused.
func (d *Decimal) Set(text string) error {
number, err := strconv.ParseUint(text, 10, 32)
if err != nil {
return err
}
*d = Decimal(number)
return nil
}
// Type is what --help shows as the flag's type, and what GetUint32
// checks before it reads the value back.
func (d *Decimal) Type() string {
return "uint32"
}