mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
339 lines
8.2 KiB
Go
339 lines
8.2 KiB
Go
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package transfer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/emmansun/base64"
|
|
|
|
"github.com/kovidgoyal/kitty"
|
|
"github.com/kovidgoyal/kitty/tools/utils"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
type Serializable interface {
|
|
String() string
|
|
MarshalJSON() ([]byte, error)
|
|
}
|
|
|
|
type Unserializable interface {
|
|
SetString(string) error
|
|
}
|
|
|
|
type Action int // enum
|
|
|
|
var _ Serializable = Action_cancel
|
|
var _ Unserializable = (*Action)(nil)
|
|
|
|
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 // enum
|
|
|
|
var _ Serializable = Compression_none
|
|
var _ Unserializable = (*Compression)(nil)
|
|
|
|
const (
|
|
Compression_none Compression = iota
|
|
Compression_zlib
|
|
)
|
|
|
|
type FileType int // enum
|
|
|
|
var _ Serializable = FileType_regular
|
|
var _ Unserializable = (*FileType)(nil)
|
|
|
|
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 ""
|
|
}
|
|
|
|
type TransmissionType int // enum
|
|
|
|
var _ Serializable = TransmissionType_simple
|
|
var _ Unserializable = (*TransmissionType)(nil)
|
|
|
|
const (
|
|
TransmissionType_simple TransmissionType = iota
|
|
TransmissionType_rsync
|
|
)
|
|
|
|
type QuietLevel int // enum
|
|
|
|
var _ Serializable = Quiet_none
|
|
var _ Unserializable = (*QuietLevel)(nil)
|
|
|
|
const (
|
|
Quiet_none QuietLevel = iota // 0
|
|
Quiet_acknowledgements // 1
|
|
Quiet_errors // 2
|
|
)
|
|
|
|
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 int64 `json:"sz,omitempty" default:"-1"`
|
|
|
|
Data []byte `json:"d,omitempty"`
|
|
}
|
|
|
|
var ftc_field_map = sync.OnceValue(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
|
|
})
|
|
|
|
var safe_string_pat = sync.OnceValue(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)
|
|
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 = safe_string(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.Int64:
|
|
if ival := val.Int(); ival != 0 && (ival > 0 || name != "sz") {
|
|
encoded_val = strconv.FormatInt(ival, 10)
|
|
}
|
|
default:
|
|
if val.CanInterface() {
|
|
switch field := val.Interface().(type) {
|
|
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{}
|
|
v := reflect.Indirect(reflect.ValueOf(ans))
|
|
if err = utils.SetStructDefaults(v); err != nil {
|
|
return
|
|
}
|
|
field_map := ftc_field_map()
|
|
key_length, key_start, val_start := 0, 0, 0
|
|
|
|
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() {
|
|
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:
|
|
val.SetString(safe_string(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.Int64:
|
|
b, err := strconv.ParseInt(serialized_val, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("The field %#v has invalid integer value with error: %w", key, err)
|
|
}
|
|
val.SetInt(b)
|
|
default:
|
|
if val.CanAddr() {
|
|
switch field := val.Addr().Interface().(type) {
|
|
case Unserializable:
|
|
err = field.SetString(serialized_val)
|
|
if err != nil {
|
|
return fmt.Errorf("The field %#v has invalid enum value with error: %w", key, err)
|
|
}
|
|
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
|
|
}
|
|
} else {
|
|
if ch == ';' {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
if key_length > 0 && val_start > 0 {
|
|
err = handle_value(serialized[key_start:key_start+key_length], serialized[val_start:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func split_for_transfer(data []byte, file_id string, mark_last bool, callback func(*FileTransmissionCommand)) {
|
|
const chunk_size = 4096
|
|
for len(data) > 0 {
|
|
chunk := data
|
|
if len(chunk) > chunk_size {
|
|
chunk = data[:chunk_size]
|
|
}
|
|
data = data[len(chunk):]
|
|
callback(&FileTransmissionCommand{
|
|
Action: utils.IfElse(mark_last && len(data) == 0, Action_end_data, Action_data),
|
|
File_id: file_id, Data: chunk})
|
|
}
|
|
}
|