ftc.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package transfer
  3. import (
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "io/fs"
  8. "reflect"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "time"
  14. "kitty"
  15. "kitty/tools/utils"
  16. )
  17. var _ = fmt.Print
  18. type Serializable interface {
  19. String() string
  20. MarshalJSON() ([]byte, error)
  21. }
  22. type Unserializable interface {
  23. SetString(string) error
  24. }
  25. type Action int // enum
  26. var _ Serializable = Action_cancel
  27. var _ Unserializable = (*Action)(nil)
  28. const (
  29. Action_invalid Action = iota
  30. Action_file
  31. Action_data
  32. Action_end_data
  33. Action_receive
  34. Action_send
  35. Action_cancel
  36. Action_status
  37. Action_finish
  38. )
  39. type Compression int // enum
  40. var _ Serializable = Compression_none
  41. var _ Unserializable = (*Compression)(nil)
  42. const (
  43. Compression_none Compression = iota
  44. Compression_zlib
  45. )
  46. type FileType int // enum
  47. var _ Serializable = FileType_regular
  48. var _ Unserializable = (*FileType)(nil)
  49. const (
  50. FileType_regular FileType = iota
  51. FileType_symlink
  52. FileType_directory
  53. FileType_link
  54. )
  55. func (self FileType) ShortText() string {
  56. switch self {
  57. case FileType_regular:
  58. return "fil"
  59. case FileType_directory:
  60. return "dir"
  61. case FileType_symlink:
  62. return "sym"
  63. case FileType_link:
  64. return "lnk"
  65. }
  66. return "und"
  67. }
  68. func (self FileType) Color() string {
  69. switch self {
  70. case FileType_regular:
  71. return "yellow"
  72. case FileType_directory:
  73. return "magenta"
  74. case FileType_symlink:
  75. return "blue"
  76. case FileType_link:
  77. return "green"
  78. }
  79. return ""
  80. }
  81. type TransmissionType int // enum
  82. var _ Serializable = TransmissionType_simple
  83. var _ Unserializable = (*TransmissionType)(nil)
  84. const (
  85. TransmissionType_simple TransmissionType = iota
  86. TransmissionType_rsync
  87. )
  88. type QuietLevel int // enum
  89. var _ Serializable = Quiet_none
  90. var _ Unserializable = (*QuietLevel)(nil)
  91. const (
  92. Quiet_none QuietLevel = iota // 0
  93. Quiet_acknowledgements // 1
  94. Quiet_errors // 2
  95. )
  96. type FileTransmissionCommand struct {
  97. Action Action `json:"ac,omitempty"`
  98. Compression Compression `json:"zip,omitempty"`
  99. Ftype FileType `json:"ft,omitempty"`
  100. Ttype TransmissionType `json:"tt,omitempty"`
  101. Quiet QuietLevel `json:"q,omitempty"`
  102. Id string `json:"id,omitempty"`
  103. File_id string `json:"fid,omitempty"`
  104. Bypass string `json:"pw,omitempty" encoding:"base64"`
  105. Name string `json:"n,omitempty" encoding:"base64"`
  106. Status string `json:"st,omitempty" encoding:"base64"`
  107. Parent string `json:"pr,omitempty"`
  108. Mtime time.Duration `json:"mod,omitempty"`
  109. Permissions fs.FileMode `json:"prm,omitempty"`
  110. Size int64 `json:"sz,omitempty" default:"-1"`
  111. Data []byte `json:"d,omitempty"`
  112. }
  113. var ftc_field_map = sync.OnceValue(func() map[string]reflect.StructField {
  114. ans := make(map[string]reflect.StructField)
  115. self := FileTransmissionCommand{}
  116. v := reflect.ValueOf(self)
  117. typ := v.Type()
  118. fields := reflect.VisibleFields(typ)
  119. for _, field := range fields {
  120. if name := field.Tag.Get("json"); name != "" && field.IsExported() {
  121. name, _, _ = strings.Cut(name, ",")
  122. ans[name] = field
  123. }
  124. }
  125. return ans
  126. })
  127. var safe_string_pat = sync.OnceValue(func() *regexp.Regexp {
  128. return regexp.MustCompile(`[^0-9a-zA-Z_:./@-]`)
  129. })
  130. func safe_string(x string) string {
  131. return safe_string_pat().ReplaceAllLiteralString(x, ``)
  132. }
  133. func (self FileTransmissionCommand) Serialize(prefix_with_osc_code ...bool) string {
  134. ans := strings.Builder{}
  135. v := reflect.ValueOf(self)
  136. found := false
  137. if len(prefix_with_osc_code) > 0 && prefix_with_osc_code[0] {
  138. ans.WriteString(strconv.Itoa(kitty.FileTransferCode))
  139. found = true
  140. }
  141. for name, field := range ftc_field_map() {
  142. val := v.FieldByIndex(field.Index)
  143. encoded_val := ""
  144. switch val.Kind() {
  145. case reflect.String:
  146. if sval := val.String(); sval != "" {
  147. enc := field.Tag.Get("encoding")
  148. switch enc {
  149. case "base64":
  150. encoded_val = base64.RawStdEncoding.EncodeToString(utils.UnsafeStringToBytes(sval))
  151. default:
  152. encoded_val = safe_string(sval)
  153. }
  154. }
  155. case reflect.Slice:
  156. switch val.Type().Elem().Kind() {
  157. case reflect.Uint8:
  158. if bval := val.Bytes(); len(bval) > 0 {
  159. encoded_val = base64.RawStdEncoding.EncodeToString(bval)
  160. }
  161. }
  162. case reflect.Int64:
  163. if ival := val.Int(); ival != 0 && (ival > 0 || name != "sz") {
  164. encoded_val = strconv.FormatInt(ival, 10)
  165. }
  166. default:
  167. if val.CanInterface() {
  168. switch field := val.Interface().(type) {
  169. case fs.FileMode:
  170. if field = field.Perm(); field != 0 {
  171. encoded_val = strconv.FormatInt(int64(field), 10)
  172. }
  173. case Serializable:
  174. if !val.Equal(reflect.Zero(val.Type())) {
  175. encoded_val = field.String()
  176. }
  177. }
  178. }
  179. }
  180. if encoded_val != "" {
  181. if found {
  182. ans.WriteString(";")
  183. } else {
  184. found = true
  185. }
  186. ans.WriteString(name)
  187. ans.WriteString("=")
  188. ans.WriteString(encoded_val)
  189. }
  190. }
  191. return ans.String()
  192. }
  193. func (self FileTransmissionCommand) String() string {
  194. s := self
  195. s.Data = nil
  196. ans, _ := json.Marshal(s)
  197. return utils.UnsafeBytesToString(ans)
  198. }
  199. func NewFileTransmissionCommand(serialized string) (ans *FileTransmissionCommand, err error) {
  200. ans = &FileTransmissionCommand{}
  201. v := reflect.Indirect(reflect.ValueOf(ans))
  202. if err = utils.SetStructDefaults(v); err != nil {
  203. return
  204. }
  205. field_map := ftc_field_map()
  206. key_length, key_start, val_start := 0, 0, 0
  207. handle_value := func(key, serialized_val string) error {
  208. key = strings.TrimLeft(key, `;`)
  209. if field, ok := field_map[key]; ok {
  210. val := v.FieldByIndex(field.Index)
  211. switch val.Kind() {
  212. case reflect.String:
  213. switch field.Tag.Get("encoding") {
  214. case "base64":
  215. b, err := base64.RawStdEncoding.DecodeString(serialized_val)
  216. if err != nil {
  217. return fmt.Errorf("The field %#v has invalid base64 encoded value with error: %w", key, err)
  218. }
  219. val.SetString(utils.UnsafeBytesToString(b))
  220. default:
  221. val.SetString(safe_string(serialized_val))
  222. }
  223. case reflect.Slice:
  224. switch val.Type().Elem().Kind() {
  225. case reflect.Uint8:
  226. b, err := base64.RawStdEncoding.DecodeString(serialized_val)
  227. if err != nil {
  228. return fmt.Errorf("The field %#v has invalid base64 encoded value with error: %w", key, err)
  229. }
  230. val.SetBytes(b)
  231. }
  232. case reflect.Int64:
  233. b, err := strconv.ParseInt(serialized_val, 10, 64)
  234. if err != nil {
  235. return fmt.Errorf("The field %#v has invalid integer value with error: %w", key, err)
  236. }
  237. val.SetInt(b)
  238. default:
  239. if val.CanAddr() {
  240. switch field := val.Addr().Interface().(type) {
  241. case Unserializable:
  242. err = field.SetString(serialized_val)
  243. if err != nil {
  244. return fmt.Errorf("The field %#v has invalid enum value with error: %w", key, err)
  245. }
  246. case *fs.FileMode:
  247. b, err := strconv.ParseUint(serialized_val, 10, 32)
  248. if err != nil {
  249. return fmt.Errorf("The field %#v has invalid file mode value with error: %w", key, err)
  250. }
  251. *field = fs.FileMode(b).Perm()
  252. }
  253. }
  254. }
  255. return nil
  256. } else {
  257. return fmt.Errorf("The field name %#v is not known", key)
  258. }
  259. }
  260. for i := 0; i < len(serialized); i++ {
  261. ch := serialized[i]
  262. if key_length == 0 {
  263. if ch == '=' {
  264. key_length = i - key_start
  265. val_start = i + 1
  266. }
  267. } else {
  268. if ch == ';' {
  269. val_length := i - val_start
  270. if key_length > 0 && val_start > 0 {
  271. err = handle_value(serialized[key_start:key_start+key_length], serialized[val_start:val_start+val_length])
  272. if err != nil {
  273. return nil, err
  274. }
  275. }
  276. key_length = 0
  277. key_start = i + 1
  278. val_start = 0
  279. }
  280. }
  281. }
  282. if key_length > 0 && val_start > 0 {
  283. err = handle_value(serialized[key_start:key_start+key_length], serialized[val_start:])
  284. if err != nil {
  285. return nil, err
  286. }
  287. }
  288. return
  289. }
  290. func split_for_transfer(data []byte, file_id string, mark_last bool, callback func(*FileTransmissionCommand)) {
  291. const chunk_size = 4096
  292. for len(data) > 0 {
  293. chunk := data
  294. if len(chunk) > chunk_size {
  295. chunk = data[:chunk_size]
  296. }
  297. data = data[len(chunk):]
  298. callback(&FileTransmissionCommand{
  299. Action: utils.IfElse(mark_last && len(data) == 0, Action_end_data, Action_data),
  300. File_id: file_id, Data: chunk})
  301. }
  302. }