// License: GPLv3 Copyright: 2023, Kovid Goyal, package transfer import ( "encoding/base64" "encoding/json" "fmt" "io/fs" "kitty" "kitty/tools/utils" "kitty/tools/wcswidth" "reflect" "strconv" "strings" "time" ) var _ = fmt.Print type Serializable interface { String() string MarshalJSON() ([]byte, error) } type Unserializable interface { Unserialize(string) error } type Action int var _ Serializable = Action_cancel var _ Unserializable = (*Action)(nil) func (self Action) MarshalJSON() ([]byte, error) { return json.Marshal(self.String()) } func (self Action) String() string { switch self { default: return "invalid" case Action_file: return "file" case Action_data: return "data" case Action_end_data: return "end_data" case Action_receive: return "receive" case Action_send: return "send" case Action_cancel: return "cancel" case Action_status: return "status" case Action_finish: 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 Action_file Action_data Action_end_data Action_receive Action_send Action_cancel Action_status Action_finish ) type Compression int var _ Serializable = Compression_none var _ Unserializable = (*Compression)(nil) const ( Compression_none Compression = iota Compression_zlib ) func (self Compression) MarshalJSON() ([]byte, error) { return json.Marshal(self.String()) } func (self Compression) String() string { switch self { default: return "none" case Compression_zlib: 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 var _ Serializable = FileType_regular var _ Unserializable = (*FileType)(nil) func (self FileType) MarshalJSON() ([]byte, error) { return json.Marshal(self.String()) } const ( FileType_regular FileType = iota FileType_symlink FileType_directory FileType_link ) func (self FileType) ShortText() string { switch self { case FileType_regular: return "fil" case FileType_directory: return "dir" case FileType_symlink: return "sym" case FileType_link: return "lnk" } return "und" } func (self FileType) Color() string { switch self { case FileType_regular: return "yellow" case FileType_directory: return "magenta" case FileType_symlink: return "blue" case FileType_link: return "green" } return "" } func (self FileType) String() string { switch self { default: return "regular" case FileType_directory: return "directory" case FileType_symlink: return "symlink" case FileType_link: 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 func (self TransmissionType) MarshalJSON() ([]byte, error) { return json.Marshal(self.String()) } var _ Serializable = TransmissionType_simple var _ Unserializable = (*TransmissionType)(nil) const ( TransmissionType_simple TransmissionType = iota TransmissionType_rsync ) func (self TransmissionType) String() string { switch self { default: return "simple" case TransmissionType_rsync: 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 func (self QuietLevel) MarshalJSON() ([]byte, error) { return json.Marshal(self.String()) } var _ Serializable = Quiet_none var _ Unserializable = (*QuietLevel)(nil) const ( Quiet_none QuietLevel = iota Quiet_acknowledgements Quiet_errors ) func (self QuietLevel) String() string { switch self { default: return "0" case Quiet_acknowledgements: return "1" case Quiet_errors: 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 `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 `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 `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) found := false if len(prefix_with_osc_code) > 0 && prefix_with_osc_code[0] { ans.WriteString(strconv.Itoa(kitty.FileTransferCode)) found = true } 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 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 Serializable: if !val.Equal(reflect.Zero(val.Type())) { encoded_val = field.String() } } } } if encoded_val != "" { if found { ans.WriteString(";") } else { found = true } ans.WriteString(name) ans.WriteString("=") ans.WriteString(encoded_val) } } return ans.String() } func (self FileTransmissionCommand) String() string { s := self s.Data = nil ans, _ := json.Marshal(s) return utils.UnsafeBytesToString(ans) } 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 { 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++ { ch := serialized[i] if key_length == 0 { 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 } } 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 }