diff --git a/gen-go-code.py b/gen-go-code.py index 45c326772..a8096dbb6 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -167,7 +167,7 @@ def generate_completions_for_kitty() -> None: # rc command wrappers {{{ json_field_types: Dict[str, str] = { - 'bool': 'bool', 'str': 'string', 'list.str': '[]string', 'dict.str': 'map[string]string', 'float': 'float64', 'int': 'int', + 'bool': 'bool', 'str': 'escaped_string', 'list.str': '[]string', 'dict.str': 'map[string]string', 'float': 'float64', 'int': 'int', 'scroll_amount': 'any', 'spacing': 'any', 'colors': 'any', } @@ -237,7 +237,10 @@ def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> if oq in option_map: o = option_map[oq] used_options.add(oq) - jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}') + if field.field_type == 'str': + jc.append(f'payload.{field.struct_field_name} = escaped_string(options_{name}.{o.go_var_name})') + else: + jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}') elif field.field in handled_fields: pass else: @@ -247,7 +250,10 @@ def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> used_options.add('Match') o = option_map['Match'] field = unhandled[x] - jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}') + if field.field_type == 'str': + jc.append(f'payload.{field.struct_field_name} = escaped_string(options_{name}.{o.go_var_name})') + else: + jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}') del unhandled[x] if unhandled: raise SystemExit(f'Cant map fields: {", ".join(unhandled)} for cmd: {name}') diff --git a/kitty/rc/base.py b/kitty/rc/base.py index c614a7de8..662742f15 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -232,7 +232,7 @@ class ArgsHandling: dest = f'payload.{jf.capitalize()}' jt = field_types[jf] if self.first_rest: - yield f'payload.{self.first_rest[0].capitalize()} = args[0]' + yield f'payload.{self.first_rest[0].capitalize()} = escaped_string(args[0])' yield f'payload.{self.first_rest[1].capitalize()} = args[1:]' handled_fields.add(self.first_rest[0]) handled_fields.add(self.first_rest[1]) @@ -254,9 +254,9 @@ class ArgsHandling: return if jt == 'str': if c == 1: - yield f'{dest} = args[0]' + yield f'{dest} = escaped_string(args[0])' else: - yield f'{dest} = strings.Join(args, " ")' + yield f'{dest} = escaped_string(strings.Join(args, " "))' return if jt.startswith('choices.'): yield f'if len(args) != 1 {{ return fmt.Errorf("%s", "Must specify exactly 1 argument for {cmd_name}") }}' diff --git a/tools/cmd/at/main.go b/tools/cmd/at/main.go index 85ff36a4a..a08a1c2e4 100644 --- a/tools/cmd/at/main.go +++ b/tools/cmd/at/main.go @@ -12,6 +12,7 @@ import ( "reflect" "strings" "time" + "unicode/utf16" "golang.org/x/sys/unix" @@ -27,6 +28,8 @@ import ( "github.com/jamesruan/go-rfc1924/base85" ) +const lowerhex = "0123456789abcdef" + var ProtocolVersion [3]int = [3]int{0, 26, 0} type GlobalOptions struct { @@ -36,11 +39,11 @@ type GlobalOptions struct { var global_options GlobalOptions -func expand_ansi_c_escapes_in_args(args ...string) (string, error) { +func expand_ansi_c_escapes_in_args(args ...string) (escaped_string, error) { for i, x := range args { args[i] = shlex.ExpandANSICEscapes(x) } - return strings.Join(args, " "), nil + return escaped_string(strings.Join(args, " ")), nil } func set_payload_string_field(io_data *rc_io_data, field, data string) { @@ -76,6 +79,48 @@ func get_pubkey(encoded_key string) (encryption_version string, pubkey []byte, e return } +type escaped_string string + +func (s escaped_string) MarshalJSON() ([]byte, error) { + // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON + // we additionally escape all non-ascii chars so they can be safely transmitted inside an escape code + src := utf16.Encode([]rune(s)) + buf := make([]byte, 0, len(src)+128) + a := func(x ...byte) { + buf = append(buf, x...) + } + a('"') + for _, r := range src { + if ' ' <= r && r <= 126 { + buf = append(buf, byte(r)) + continue + } + switch r { + case '\n': + a('\\', 'n') + case '\t': + a('\\', 't') + case '\r': + a('\\', 'r') + case '\f': + a('\\', 'f') + case '\b': + a('\\', 'b') + case '\\': + a('\\', '\\') + case '"': + a('\\', '"') + default: + a('\\', 'u') + for s := 12; s >= 0; s -= 4 { + a(lowerhex[r>>uint(s)&0xF]) + } + } + } + a('"') + return buf, nil +} + func simple_serializer(rc *utils.RemoteControlCmd) (ans []byte, err error) { return json.Marshal(rc) } diff --git a/tools/cmd/at/main_test.go b/tools/cmd/at/main_test.go index cb7d77dad..daa5b9884 100644 --- a/tools/cmd/at/main_test.go +++ b/tools/cmd/at/main_test.go @@ -10,6 +10,25 @@ import ( "testing" ) +func TestEncodeJSON(t *testing.T) { + tests := map[string]string{ + "a b\nc\td\a": `a b\nc\td\u0007`, + "•": `\u2022`, + "\U0001f123": `\ud83c\udd23`, + } + var s escaped_string + for x, expected := range tests { + s = escaped_string(x) + expected = `"` + expected + `"` + actualb, _ := s.MarshalJSON() + actual := string(actualb) + if expected != actual { + t.Fatalf("Failed for %#v\n%#v != %#v", x, expected, actual) + } + } + +} + func TestCommandToJSON(t *testing.T) { pv := fmt.Sprint(ProtocolVersion[0], ",", ProtocolVersion[1], ",", ProtocolVersion[2]) rc, err := create_rc_ls([]string{})