File: ftc.go

package info (click to toggle)
kitty 0.45.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 27,476 kB
  • sloc: ansic: 84,285; python: 57,992; objc: 5,432; sh: 1,333; xml: 364; makefile: 144; javascript: 78
file content (338 lines) | stat: -rw-r--r-- 8,405 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>

package transfer

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/fs"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"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})
	}
}