diff --git a/kittens/transfer/ftc.go b/kittens/transfer/ftc.go index 61137b5ef..87795d38b 100644 --- a/kittens/transfer/ftc.go +++ b/kittens/transfer/ftc.go @@ -7,13 +7,14 @@ import ( "encoding/json" "fmt" "io/fs" - "kitty" - "kitty/tools/utils" - "kitty/tools/wcswidth" "reflect" + "regexp" "strconv" "strings" "time" + + "kitty" + "kitty/tools/utils" ) var _ = fmt.Print @@ -154,6 +155,14 @@ var ftc_field_map = utils.Once(func() map[string]reflect.StructField { return ans }) +var safe_string_pat = utils.Once(func() *regexp.Regexp { + return regexp.MustCompile(`[^0-9a-zA-Z_:.,/!@#$%^&*()[\]{}~` + "`" + `?"'\\|=+-]`) +}) + +func safe_string(x string) string { + return safe_string_pat().ReplaceAllLiteralString(x, ``) +} + func (self *FileTransmissionCommand) Serialize(prefix_with_osc_code ...bool) string { ans := strings.Builder{} v := reflect.ValueOf(*self) @@ -173,7 +182,7 @@ func (self *FileTransmissionCommand) Serialize(prefix_with_osc_code ...bool) str case "base64": encoded_val = base64.RawStdEncoding.EncodeToString(utils.UnsafeStringToBytes(sval)) default: - encoded_val = escape_semicolons(wcswidth.StripEscapeCodes(sval)) + encoded_val = safe_string(sval) } } case reflect.Slice: @@ -229,11 +238,11 @@ func (self FileTransmissionCommand) String() string { func NewFileTransmissionCommand(serialized string) (ans *FileTransmissionCommand, err error) { ans = &FileTransmissionCommand{} key_length, key_start, val_start, val_length := 0, 0, 0, 0 - has_semicolons := false field_map := ftc_field_map() v := reflect.Indirect(reflect.ValueOf(ans)) - handle_value := func(key, serialized_val string, has_semicolons bool) error { + handle_value := func(key, serialized_val string) error { + key = strings.TrimLeft(key, `;;`) if field, ok := field_map[key]; ok { val := v.FieldByIndex(field.Index) switch val.Kind() { @@ -246,10 +255,7 @@ func NewFileTransmissionCommand(serialized string) (ans *FileTransmissionCommand } val.SetString(utils.UnsafeBytesToString(b)) default: - if has_semicolons { - serialized_val = strings.ReplaceAll(serialized_val, `;;`, `;`) - } - val.SetString(serialized_val) + val.SetString(safe_string(serialized_val)) } case reflect.Slice: switch val.Type().Elem().Kind() { @@ -302,31 +308,25 @@ func NewFileTransmissionCommand(serialized string) (ans *FileTransmissionCommand if ch == '=' { key_length = i - key_start val_start = i + 1 - has_semicolons = false } } else { if ch == ';' { - if i+1 < len(serialized) && serialized[i+1] == ';' { - has_semicolons = true - i++ - } else { - val_length = i - val_start - if key_length > 0 && val_start > 0 { - err = handle_value(serialized[key_start:key_start+key_length], serialized[val_start:val_start+val_length], has_semicolons) - if err != nil { - return nil, err - } + val_length = i - val_start + if key_length > 0 && val_start > 0 { + err = handle_value(serialized[key_start:key_start+key_length], serialized[val_start:val_start+val_length]) + if err != nil { + return nil, err } - key_length = 0 - key_start = i + 1 - val_start = 0 - val_length = 0 } + key_length = 0 + key_start = i + 1 + val_start = 0 + val_length = 0 } } } if key_length > 0 && val_start > 0 { - err = handle_value(serialized[key_start:key_start+key_length], serialized[val_start:], has_semicolons) + err = handle_value(serialized[key_start:key_start+key_length], serialized[val_start:]) if err != nil { return nil, err } diff --git a/kittens/transfer/ftc_test.go b/kittens/transfer/ftc_test.go index a6a07debd..51787ce39 100644 --- a/kittens/transfer/ftc_test.go +++ b/kittens/transfer/ftc_test.go @@ -43,4 +43,9 @@ func TestFTCSerialization(t *testing.T) { t.Fatal(err) } q(n.Serialize()) + + unsafe := "moo\x1b;;[?*.-se1" + if safe_string(unsafe) != "moo[?*.-se1" { + t.Fatalf("safe_string() failed for %#v yielding: %#v", unsafe, safe_string(unsafe)) + } } diff --git a/kittens/transfer/rsync.c b/kittens/transfer/rsync.c index 2e736e40b..c90b26a98 100644 --- a/kittens/transfer/rsync.c +++ b/kittens/transfer/rsync.c @@ -162,12 +162,13 @@ begin_patch(PyObject *self UNUSED, PyObject *callback) { } static bool -call_ftc_callback(PyObject *callback, char *src, Py_ssize_t key_start, Py_ssize_t key_length, Py_ssize_t val_start, Py_ssize_t val_length, PyObject *has_semicolons) { +call_ftc_callback(PyObject *callback, char *src, Py_ssize_t key_start, Py_ssize_t key_length, Py_ssize_t val_start, Py_ssize_t val_length) { + while(src[key_start] == ';' && key_length > 0 ) { key_start++; key_length--; } DECREF_AFTER_FUNCTION PyObject *k = PyMemoryView_FromMemory(src + key_start, key_length, PyBUF_READ); if (!k) return false; DECREF_AFTER_FUNCTION PyObject *v = PyMemoryView_FromMemory(src + val_start, val_length, PyBUF_READ); if (!v) return false; - DECREF_AFTER_FUNCTION PyObject *ret = PyObject_CallFunctionObjArgs(callback, k, v, has_semicolons, NULL); + DECREF_AFTER_FUNCTION PyObject *ret = PyObject_CallFunctionObjArgs(callback, k, v, NULL); return ret != NULL; } @@ -187,31 +188,24 @@ parse_ftc(PyObject *self UNUSED, PyObject *args) { char *src = buf.buf; size_t sz = buf.len; if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback must be callable"); return NULL; } - PyObject *has_semicolons = Py_False; for (i = 0; i < sz; i++) { char ch = src[i]; if (key_length == 0) { if (ch == '=') { key_length = i - key_start; val_start = i + 1; - has_semicolons = Py_False; } } else { if (ch == ';') { - if (i + 1 < sz && src[i + 1] == ';') { - has_semicolons = Py_True; - i++; - } else { - val_length = i - val_start; - if (!call_ftc_callback(callback, src, key_start, key_length, val_start, val_length, has_semicolons)) return NULL; - key_length = 0; key_start = i + 1; val_start = 0; - } + val_length = i - val_start; + if (!call_ftc_callback(callback, src, key_start, key_length, val_start, val_length)) return NULL; + key_length = 0; key_start = i + 1; val_start = 0; } } } if (key_length && val_start) { val_length = sz - val_start; - if (!call_ftc_callback(callback, src, key_start, key_length, val_start, val_length, has_semicolons)) return NULL; + if (!call_ftc_callback(callback, src, key_start, key_length, val_start, val_length)) return NULL; } Py_RETURN_NONE; } diff --git a/kittens/transfer/rsync.pyi b/kittens/transfer/rsync.pyi index fb648e5b2..fb17ddcb0 100644 --- a/kittens/transfer/rsync.pyi +++ b/kittens/transfer/rsync.pyi @@ -39,7 +39,7 @@ def iter_job(job_capsule: JobCapsule, input_data: bytes, output_buf: bytearray) pass -def parse_ftc(src: Union[str, bytes, memoryview], callback: Callable[[memoryview, memoryview, bool], None]) -> None: +def parse_ftc(src: Union[str, bytes, memoryview], callback: Callable[[memoryview, memoryview], None]) -> None: pass diff --git a/kitty/file_transmission.py b/kitty/file_transmission.py index 2d9e86edb..97b781e62 100644 --- a/kitty/file_transmission.py +++ b/kitty/file_transmission.py @@ -29,10 +29,7 @@ ftc_prefix = str(FILE_TRANSFER_CODE) -def escape_semicolons(x: str) -> str: - return x.replace(';', ';;') - - +@run_once def safe_string_pat() -> 're.Pattern[str]': return re.compile(r'[^0-9a-zA-Z_:./@-]') @@ -316,7 +313,7 @@ def get_serialized_fields(self, prefix_with_osc_code: bool = False) -> Iterator[ if k.metadata.get('base64'): yield standard_b64encode(val.encode('utf-8')) else: - yield escape_semicolons(safe_string(val)) + yield safe_string(val) elif k.type is int: yield str(val) else: @@ -331,7 +328,7 @@ def deserialize(cls, data: Union[str, bytes, memoryview]) -> 'FileTransmissionCo fmap = serialized_to_field_map() from kittens.transfer.rsync import decode_utf8_buffer, parse_ftc - def handle_item(key: memoryview, val: memoryview, has_semicolons: bool) -> None: + def handle_item(key: memoryview, val: memoryview) -> None: field = fmap.get(key) if field is None: return @@ -345,9 +342,7 @@ def handle_item(key: memoryview, val: memoryview, has_semicolons: bool) -> None: if field.metadata.get('base64'): sval = standard_b64decode(val).decode('utf-8') else: - sval = decode_utf8_buffer(val) - if has_semicolons: - sval = sval.replace(';;', ';') + sval = safe_string(decode_utf8_buffer(val)) setattr(ans, field.name, safe_string(sval)) parse_ftc(data, handle_item) diff --git a/kitty_tests/file_transmission.py b/kitty_tests/file_transmission.py index 80310957a..07715afbc 100644 --- a/kitty_tests/file_transmission.py +++ b/kitty_tests/file_transmission.py @@ -353,10 +353,8 @@ def test_parse_ftc(self): def t(raw, *expected): a = [] - def c(k, v, has_semicolons): + def c(k, v): a.append(decode_utf8_buffer(k)) - if has_semicolons: - v = bytes(v).replace(b';;', b';') a.append(decode_utf8_buffer(v)) parse_ftc(raw, c) @@ -364,9 +362,9 @@ def c(k, v, has_semicolons): t('a=b', 'a', 'b') t('a=b;', 'a', 'b') - t('a1=b1;c=d;;', 'a1', 'b1', 'c', 'd;') - t('a1=b1;c=d;;e', 'a1', 'b1', 'c', 'd;e') - t('a1=b1;c=d;;;1=1', 'a1', 'b1', 'c', 'd;', '1', '1') + t('a1=b1;c=d;;', 'a1', 'b1', 'c', 'd') + t('a1=b1;c=d;;e', 'a1', 'b1', 'c', 'd') + t('a1=b1;c=d;;;1=1', 'a1', 'b1', 'c', 'd', '1', '1') def test_path_mapping_receive(self): opts = parse_transfer_args([])[0]