From 19e74da5b20b6362b07fa243842cdc4b9aadae15 Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Sun, 30 Aug 2026 08:01:20 +0800 Subject: [PATCH] fix(input): reject input macro keys ending in a repeat suffix parseSuffixRepeat splits a braced input macro token at the last '*' followed by digits. When the content carries two repeat suffixes the leftover key name still ends in one, so expandInputMacroExt emitted a "{name}" token that Command.String() could not write back: re-reading it stripped the suffix a second time. "{*1*1}" serialised to "{*1}" and failed to reparse, while "{a*1*2}" serialised to two "{a*1}" tokens that read back as two "a" tokens. Reject the ambiguous form at parse time instead of producing a token that cannot round-trip. Names where '*' is literal ("{ctrl+*}", "{a*b}", "{a*0}", "{*}") are unaffected. --- arguments.go | 6 ++++++ parser_input_macro_test.go | 20 +++++++++++++++++++ symbols.go | 1 + .../fuzz/FuzzCommandString/8b2c3a3ac03788e5 | 2 ++ 4 files changed, 29 insertions(+) create mode 100644 testdata/fuzz/FuzzCommandString/8b2c3a3ac03788e5 diff --git a/arguments.go b/arguments.go index e36595d..22f67be 100644 --- a/arguments.go +++ b/arguments.go @@ -268,6 +268,12 @@ func expandInputMacroExt(content string, totalLen *int) ([]string, error) { if name == "" { return nil, ErrInputMacroEmptyKey } + // A name that still ends in a repeat suffix (e.g. "{a*1*2}" leaves "a*1") + // cannot be written back as "{name}": re-reading would strip the suffix a + // second time and yield a different macro. Reject the ambiguous form. + if stripped, _, stripErr := parseSuffixRepeat(name); stripErr != nil || stripped != name { + return nil, ErrInputMacroAmbiguousKey + } // Single-rune keys are appended without braces (e.g. "a", "*"). // Multi-rune names need braces so ParseKeyCombo recognises them. diff --git a/parser_input_macro_test.go b/parser_input_macro_test.go index b5b0161..a06dea0 100644 --- a/parser_input_macro_test.go +++ b/parser_input_macro_test.go @@ -122,6 +122,16 @@ func TestInputMacroGrammar(t *testing.T) { input: "**input.keyboard:{a*b}", want: kbd("{a*b}"), }, + { + name: "trailing asterisk in combo is literal", + input: "**input.keyboard:{ctrl+*}", + want: kbd("{ctrl+*}"), + }, + { + name: "asterisk before zero is literal", + input: "**input.keyboard:{a*0}", + want: kbd("{a*0}"), + }, { name: "repeat in mixed sequence", input: "**input.keyboard:a{enter*2}b", @@ -347,6 +357,16 @@ func TestInputMacroCaps(t *testing.T) { input: "**input.keyboard:{*5}", wantErr: zapscript.ErrInputMacroEmptyKey, }, + { + name: "key name left with a repeat suffix", + input: "**input.keyboard:{a*1*2}", + wantErr: zapscript.ErrInputMacroAmbiguousKey, + }, + { + name: "repeat suffix only key name", + input: "**input.keyboard:{*1*1}", + wantErr: zapscript.ErrInputMacroAmbiguousKey, + }, { name: "unclosed quoted literal at EOF", input: `**input.keyboard:{"unclosed`, diff --git a/symbols.go b/symbols.go index e844222..6f2af1c 100644 --- a/symbols.go +++ b/symbols.go @@ -38,6 +38,7 @@ var ( ErrInputMacroRepeatTooLarge = errors.New("input macro repeat count exceeds maximum") ErrInputMacroTooLong = errors.New("input macro expanded key count exceeds maximum") ErrInputMacroEmptyKey = errors.New("input macro key name is empty after repeat suffix removal") + ErrInputMacroAmbiguousKey = errors.New("input macro key name ends in a repeat suffix") ) const ( diff --git a/testdata/fuzz/FuzzCommandString/8b2c3a3ac03788e5 b/testdata/fuzz/FuzzCommandString/8b2c3a3ac03788e5 new file mode 100644 index 0000000..bea0d20 --- /dev/null +++ b/testdata/fuzz/FuzzCommandString/8b2c3a3ac03788e5 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("**input.keYBoArd:{*1*1}")