main pico / pkg / send / protocols / scp / copy_from_client.go
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}