From b9b7ab5221e84fc70dad5bc241532c195ee86526 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 May 2023 10:15:17 +0530 Subject: [PATCH] Finish parsing of FTC --- kittens/transfer/ftc.go | 325 +++++++++++++++++++++++++---------- kittens/transfer/ftc_test.go | 16 +- 2 files changed, 250 insertions(+), 91 deletions(-) diff --git a/kittens/transfer/ftc.go b/kittens/transfer/ftc.go index 65f1af0c6..e54ad8fe1 100644 --- a/kittens/transfer/ftc.go +++ b/kittens/transfer/ftc.go @@ -17,15 +17,18 @@ import ( var _ = fmt.Print -type Field interface { +type Serializable interface { IsDefault() bool - Serialize() string + String() string +} +type Unserializable interface { + Unserialize(string) error } type Action int func (self Action) IsDefault() bool { return self == Action_invalid } -func (self Action) Serialize() string { +func (self Action) String() string { switch self { default: return "invalid" @@ -47,6 +50,31 @@ func (self Action) Serialize() string { return "finish" } } +func (self *Action) Unserialize(x string) (err error) { + switch x { + case "invalid": + *self = Action_invalid + case "file": + *self = Action_file + case "data": + *self = Action_data + case "end_data": + *self = Action_end_data + case "receive": + *self = Action_receive + case "send": + *self = Action_send + case "cancel": + *self = Action_cancel + case "status": + *self = Action_status + case "finish": + *self = Action_finish + default: + err = fmt.Errorf("Unknown Action value: %#v", x) + } + return +} const ( Action_invalid Action = iota @@ -68,7 +96,7 @@ const ( ) func (self Compression) IsDefault() bool { return self == Compression_none } -func (self Compression) Serialize() string { +func (self Compression) String() string { switch self { default: return "none" @@ -76,6 +104,17 @@ func (self Compression) Serialize() string { return "zlib" } } +func (self *Compression) Unserialize(x string) (err error) { + switch x { + case "none": + *self = Compression_none + case "zlib": + *self = Compression_zlib + default: + err = fmt.Errorf("Unknown Compression value: %#v", x) + } + return +} type FileType int @@ -114,22 +153,8 @@ func (self FileType) Color() string { return "" } -func (self FileType) String() string { - switch self { - case FileType_regular: - return "FileType.Regular" - case FileType_directory: - return "FileType.Directory" - case FileType_symlink: - return "FileType.SymbolicLink" - case FileType_link: - return "FileType.Link" - } - return "FileType.Unknown" -} - func (self FileType) IsDefault() bool { return self == FileType_regular } -func (self FileType) Serialize() string { +func (self FileType) String() string { switch self { default: return "regular" @@ -141,6 +166,21 @@ func (self FileType) Serialize() string { return "link" } } +func (self *FileType) Unserialize(x string) (err error) { + switch x { + case "regular": + *self = FileType_regular + case "directory": + *self = FileType_directory + case "symlink": + *self = FileType_symlink + case "link": + *self = FileType_link + default: + err = fmt.Errorf("Unknown FileType value: %#v", x) + } + return +} type TransmissionType int @@ -150,7 +190,7 @@ const ( ) func (self TransmissionType) IsDefault() bool { return self == TransmissionType_simple } -func (self TransmissionType) Serialize() string { +func (self TransmissionType) String() string { switch self { default: return "simple" @@ -158,6 +198,17 @@ func (self TransmissionType) Serialize() string { return "rsync" } } +func (self *TransmissionType) Unserialize(x string) (err error) { + switch x { + case "simple": + *self = TransmissionType_simple + case "rsync": + *self = TransmissionType_rsync + default: + err = fmt.Errorf("Unknown TransmissionType value: %#v", x) + } + return +} type QuietLevel int @@ -168,7 +219,7 @@ const ( ) func (self QuietLevel) IsDefault() bool { return self == Quiet_none } -func (self QuietLevel) Serialize() string { +func (self QuietLevel) String() string { switch self { default: return "0" @@ -178,95 +229,119 @@ func (self QuietLevel) Serialize() string { return "2" } } +func (self *QuietLevel) Unserialize(x string) (err error) { + switch x { + case "0": + *self = Quiet_none + case "1": + *self = Quiet_acknowledgements + case "2": + *self = Quiet_errors + default: + err = fmt.Errorf("Unknown QuietLevel value: %#v", x) + } + return +} type FileTransmissionCommand struct { - Action Action `name:"ac"` - Compression Compression `name:"zip"` - Ftype FileType `name:"ft"` - Ttype TransmissionType `name:"tt"` - Quiet QuietLevel `name:"q"` + Action Action `json:"ac,omitempty"` + Compression Compression `json:"zip,omitempty"` + Ftype FileType `json:"ft,omitempty"` + Ttype TransmissionType `json:"tt,omitempty"` + Quiet QuietLevel `json:"q,omitempty"` - Id string `name:"id"` - File_id string `name:"fid"` - Bypass string `name:"pw" encoding:"base64"` - Name string `name:"n" encoding:"base64"` - Status string `name:"st" encoding:"base64"` - Parent string `name:"pr"` - Mtime time.Duration `name:"mod"` - Permissions fs.FileMode `name:"prm"` - Size uint64 `name:"sz"` + Id string `json:"id,omitempty"` + File_id string `json:"fid,omitempty"` + Bypass string `json:"pw,omitempty" encoding:"base64"` + Name string `json:"n,omitempty" encoding:"base64"` + Status string `json:"st,omitempty" encoding:"base64"` + Parent string `json:"pr,omitempty"` + Mtime time.Duration `json:"mod,omitempty"` + Permissions fs.FileMode `json:"prm,omitempty"` + Size uint64 `json:"sz,omitempty"` - Data []byte `name:"d"` + Data []byte `json:"d,omitempty"` } func escape_semicolons(x string) string { return strings.ReplaceAll(x, ";", ";;") } +var ftc_field_map = utils.Once(func() map[string]reflect.StructField { + ans := make(map[string]reflect.StructField) + self := FileTransmissionCommand{} + v := reflect.ValueOf(self) + typ := v.Type() + fields := reflect.VisibleFields(typ) + for _, field := range fields { + if name := field.Tag.Get("json"); name != "" && field.IsExported() { + name, _, _ = strings.Cut(name, ",") + ans[name] = field + } + } + return ans +}) + func (self *FileTransmissionCommand) Serialize(prefix_with_osc_code ...bool) string { ans := strings.Builder{} v := reflect.ValueOf(*self) - typ := v.Type() - fields := reflect.VisibleFields(typ) found := false if len(prefix_with_osc_code) > 0 && prefix_with_osc_code[0] { ans.WriteString(strconv.Itoa(kitty.FileTransferCode)) found = true } - for _, field := range fields { - if name := field.Tag.Get("name"); name != "" && field.IsExported() { - val := v.FieldByIndex(field.Index) - encoded_val := "" - switch val.Kind() { - case reflect.String: - if sval := val.String(); sval != "" { - enc := field.Tag.Get("encoding") - switch enc { - case "base64": - encoded_val = base64.RawStdEncoding.EncodeToString(utils.UnsafeStringToBytes(sval)) - default: - encoded_val = escape_semicolons(wcswidth.StripEscapeCodes(sval)) + for name, field := range ftc_field_map() { + val := v.FieldByIndex(field.Index) + encoded_val := "" + switch val.Kind() { + case reflect.String: + if sval := val.String(); sval != "" { + enc := field.Tag.Get("encoding") + switch enc { + case "base64": + encoded_val = base64.RawStdEncoding.EncodeToString(utils.UnsafeStringToBytes(sval)) + default: + encoded_val = escape_semicolons(wcswidth.StripEscapeCodes(sval)) + } + } + case reflect.Slice: + switch val.Type().Elem().Kind() { + case reflect.Uint8: + if bval := val.Bytes(); len(bval) > 0 { + encoded_val = base64.RawStdEncoding.EncodeToString(bval) + } + } + case reflect.Uint64: + if uival := val.Uint(); uival != 0 { + encoded_val = strconv.FormatUint(uival, 10) + } + default: + if val.CanInterface() { + switch field := val.Interface().(type) { + case Serializable: + if !field.IsDefault() { + encoded_val = field.String() } - } - case reflect.Slice: - switch val.Type().Elem().Kind() { - case reflect.Uint8: - if bval := val.Bytes(); len(bval) > 0 { - encoded_val = base64.RawStdEncoding.EncodeToString(bval) + case time.Duration: + if field != 0 { + encoded_val = strconv.FormatInt(int64(field), 10) } - } - case reflect.Uint64: - if uival := val.Uint(); uival != 0 { - encoded_val = strconv.FormatUint(uival, 10) - } - default: - if val.CanInterface() { - switch field := val.Interface().(type) { - case Field: - if !field.IsDefault() { - encoded_val = field.Serialize() - } - case time.Duration: - if field != 0 { - encoded_val = strconv.FormatInt(int64(field), 10) - } - case fs.FileMode: - if field = field.Perm(); field != 0 { - encoded_val = strconv.FormatInt(int64(field), 10) - } + case fs.FileMode: + if field = field.Perm(); field != 0 { + encoded_val = strconv.FormatInt(int64(field), 10) } } } - if encoded_val != "" { - if found { - ans.WriteString(";") - } else { - found = true - } - ans.WriteString(name) - ans.WriteString("=") - ans.WriteString(encoded_val) + } + if encoded_val != "" { + if found { + ans.WriteString(";") + } else { + found = true } + ans.WriteString(name) + ans.WriteString("=") + ans.WriteString(encoded_val) } } return ans.String() @@ -276,9 +351,70 @@ func NewFileTransmissionCommand(serialized string) (ans *FileTransmissionCommand 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 { - return nil + if field, ok := field_map[key]; ok { + val := v.FieldByIndex(field.Index) + switch val.Kind() { + case reflect.String: + switch field.Tag.Get("encoding") { + case "base64": + b, err := base64.RawStdEncoding.DecodeString(serialized_val) + if err != nil { + return fmt.Errorf("The field %#v has invalid base64 encoded value with error: %w", key, err) + } + val.SetString(utils.UnsafeBytesToString(b)) + default: + if has_semicolons { + serialized_val = strings.ReplaceAll(serialized_val, `;;`, `;`) + } + val.SetString(serialized_val) + } + case reflect.Slice: + switch val.Type().Elem().Kind() { + case reflect.Uint8: + b, err := base64.RawStdEncoding.DecodeString(serialized_val) + if err != nil { + return fmt.Errorf("The field %#v has invalid base64 encoded value with error: %w", key, err) + } + val.SetBytes(b) + } + case reflect.Uint64: + b, err := strconv.ParseUint(serialized_val, 10, 64) + if err != nil { + return fmt.Errorf("The field %#v has invalid unsigned integer value with error: %w", key, err) + } + val.SetUint(b) + default: + if val.CanAddr() { + switch field := val.Addr().Interface().(type) { + case Unserializable: + err = field.Unserialize(serialized_val) + if err != nil { + return fmt.Errorf("The field %#v has invalid enum value with error: %w", key, err) + } + case *time.Duration: + b, err := strconv.ParseInt(serialized_val, 10, 64) + if err != nil { + return fmt.Errorf("The field %#v has invalid time value with error: %w", key, err) + } + *field = time.Duration(b) + case *fs.FileMode: + b, err := strconv.ParseUint(serialized_val, 10, 32) + if err != nil { + return fmt.Errorf("The field %#v has invalid file mode value with error: %w", key, err) + } + *field = fs.FileMode(b).Perm() + } + + } + } + return nil + } else { + return fmt.Errorf("The field name %#v is not known", key) + } } for i := 0; i < len(serialized); i++ { @@ -296,16 +432,25 @@ func NewFileTransmissionCommand(serialized string) (ans *FileTransmissionCommand i++ } else { val_length = i - val_start - 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 + 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 + } } 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) + if err != nil { + return nil, err + } + } return } diff --git a/kittens/transfer/ftc_test.go b/kittens/transfer/ftc_test.go index 83a2d3c81..a6a07debd 100644 --- a/kittens/transfer/ftc_test.go +++ b/kittens/transfer/ftc_test.go @@ -4,6 +4,7 @@ package transfer import ( "fmt" + "strings" "testing" "time" @@ -16,7 +17,15 @@ func TestFTCSerialization(t *testing.T) { ftc := FileTransmissionCommand{} q := func(expected string) { actual := ftc.Serialize() - if diff := cmp.Diff(expected, actual); diff != "" { + ad := make(map[string]bool) + for _, x := range strings.Split(actual, ";") { + ad[x] = true + } + ed := make(map[string]bool) + for _, x := range strings.Split(expected, ";") { + ed[x] = true + } + if diff := cmp.Diff(ed, ad); diff != "" { t.Fatalf("Failed to Serialize:\n%s", diff) } } @@ -29,4 +38,9 @@ func TestFTCSerialization(t *testing.T) { ftc.Permissions = 0o600 ftc.Data = []byte("moose") q("ac=send;fid=fid;n=bW9vc2U;mod=1000000000;prm=384;d=bW9vc2U") + n, err := NewFileTransmissionCommand(ftc.Serialize()) + if err != nil { + t.Fatal(err) + } + q(n.Serialize()) }