Antonio Mika
·
2026-10-10
1package scp
2
3import (
4 "bufio"
5 "errors"
6 "fmt"
7 "io"
8 "io/fs"
9 "path/filepath"
10 "regexp"
11 "strconv"
12 "strings"
13
14 "github.com/picosh/pico/pkg/pssh"
15 "github.com/picosh/pico/pkg/send/utils"
16)
17
18var (
19 reTimestamp = regexp.MustCompile(`^T(\d{10}) 0 (\d{10}) 0$`)
20 reNewFolder = regexp.MustCompile(`^D(\d{4}) 0 (.*)$`)
21 reNewFile = regexp.MustCompile(`^C(\d{4}) (\d+) (.*)$`)
22)
23
24type parseError struct {
25 subject string
26}
27
28func (e parseError) Error() string {
29 return fmt.Sprintf("failed to parse: %q", e.subject)
30}
31
32func copyFromClient(session *pssh.SSHServerConnSession, info Info, handler utils.CopyFromClientHandler) error {
33 // accepts the request
34 _, _ = session.Write(utils.NULL)
35
36 writeErrors := []error{}
37 writeSuccess := []string{}
38
39 var (
40 path = info.Path
41 r = bufio.NewReader(session)
42 mtime int64
43 atime int64
44 )
45
46 for {
47 line, _, err := r.ReadLine()
48 if err != nil {
49 if errors.Is(err, io.EOF) {
50 break
51 }
52 return fmt.Errorf("failed to read line: %w", err)
53 }
54
55 if matches := reTimestamp.FindAllStringSubmatch(string(line), 2); matches != nil {
56 mtime, err = strconv.ParseInt(matches[0][1], 10, 64)
57 if err != nil {
58 return parseError{string(line)}
59 }
60 atime, err = strconv.ParseInt(matches[0][2], 10, 64)
61 if err != nil {
62 return parseError{string(line)}
63 }
64
65 // accepts the header
66 _, _ = session.Write(utils.NULL)
67 continue
68 }
69
70 if matches := reNewFile.FindAllStringSubmatch(string(line), 3); matches != nil {
71 if len(matches) != 1 || len(matches[0]) != 4 {
72 return parseError{string(line)}
73 }
74
75 mode, err := strconv.ParseUint(matches[0][1], 8, 32)
76 if err != nil {
77 return parseError{string(line)}
78 }
79
80 size, err := strconv.ParseInt(matches[0][2], 10, 64)
81 if err != nil {
82 return parseError{string(line)}
83 }
84 name := matches[0][3]
85
86 // accepts the header
87 _, _ = session.Write(utils.NULL)
88
89 reader := utils.NewLimitReader(r, int(size))
90 result, err := handler.Write(session, &utils.FileEntry{
91 Filepath: filepath.Join(path, name),
92 Mode: fs.FileMode(mode),
93 Size: size,
94 Mtime: mtime,
95 Atime: atime,
96 Reader: reader,
97 })
98 // A handler that rejects a file may not read all of it. Skip
99 // the rest so the next header is read from the right place.
100 if _, derr := io.Copy(io.Discard, reader); derr != nil {
101 return fmt.Errorf("failed to read file: %q: %w", name, derr)
102 }
103
104 switch {
105 case err == nil:
106 writeSuccess = append(writeSuccess, result)
107 case errors.Is(err, utils.ErrFileIgnored):
108 writeErrors = append(writeErrors, err)
109 default:
110 fmt.Printf("failed to write file: %q: %v\n", name, err)
111 }
112
113 // read the trailing nil char
114 _, _ = r.ReadByte() // TODO: check if it is indeed a utils.NULL?
115
116 mtime = 0
117 atime = 0
118 if err != nil && !errors.Is(err, utils.ErrFileIgnored) {
119 // A warning reply makes the client report the file and
120 // exit non-zero once the rest are sent.
121 msg := strings.ReplaceAll(err.Error(), "\n", " ")
122 _, _ = fmt.Fprintf(session, "\x01%s\n", msg)
123 continue
124 }
125 // says 'hey im done'
126 _, _ = session.Write(utils.NULL)
127 continue
128 }
129
130 if matches := reNewFolder.FindAllStringSubmatch(string(line), 2); matches != nil {
131 if len(matches) != 1 || len(matches[0]) != 3 {
132 return parseError{string(line)}
133 }
134
135 name := matches[0][2]
136 path = filepath.Join(path, name)
137 // says 'hey im done'
138 _, _ = session.Write(utils.NULL)
139 continue
140 }
141
142 if string(line) == "E" {
143 path = filepath.Dir(path)
144
145 // says 'hey im done'
146 _, _ = session.Write(utils.NULL)
147 continue
148 }
149
150 return fmt.Errorf("unhandled input: %q", string(line))
151 }
152
153 utils.PrintMsg(session, writeSuccess, writeErrors)
154
155 _, _ = session.Write(utils.NULL)
156 return nil
157}