summaryrefslogtreecommitdiff
path: root/gopher/request.go
blob: fdd7480721c2ab3fee7c7bf813fc350f8a8b38e5 (plain)
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
package gopher

import (
	"bytes"
	"errors"
	"io"
	"net/url"
	"path"
	"strings"

	"tildegit.org/tjp/sliderule/internal/types"
)

// ParseRequest parses a gopher protocol request into a sliderule.Request object.
func ParseRequest(rdr io.Reader) (*types.Request, error) {
	selector, search, err := readFullRequest(rdr)
	if err != nil {
		return nil, err
	}

	if !strings.HasPrefix(selector, "/") {
		selector = "/" + selector
	}

	return &types.Request{
		URL: &url.URL{
			Scheme:   "gopher",
			Path:     path.Clean(selector),
			OmitHost: true, //nolint:typecheck
			// (for some reason typecheck on drone-ci doesn't realize OmitHost is a field in url.URL)
			RawQuery: url.QueryEscape(search),
		},
	}, nil
}

func readFullRequest(rdr io.Reader) (string, string, error) {
	// The vast majority of requests will fit in this size:
	// the specified 255 byte max for selector, then CRLF.
	buf := make([]byte, 257)

	n, err := rdr.Read(buf)
	if err != nil && !errors.Is(err, io.EOF) {
		return "", "", err
	}
	buf = buf[:n]

	// Full-text search transactions are the exception, they
	// may be longer because there is an additional search string
	if n == 257 && buf[256] != '\n' {
		intake := buf[n:cap(buf)]
		total := n
		for {
			intake = append(intake, 0)
			intake = intake[:cap(intake)]

			n, err = rdr.Read(intake)
			if err != nil && err != io.EOF {
				return "", "", err
			}
			total += n

			if n < cap(intake) || intake[cap(intake)-1] == '\n' {
				break
			}
			intake = intake[n:]
		}
		buf = buf[:total]
	}

	selector, search, _ := bytes.Cut(buf, []byte{'\t'})
	return strings.TrimRight(string(selector), "\r\n"), strings.TrimRight(string(search), "\r\n"), nil
}