summaryrefslogtreecommitdiff
path: root/gopher/request.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopher/request.go')
-rw-r--r--gopher/request.go72
1 files changed, 72 insertions, 0 deletions
diff --git a/gopher/request.go b/gopher/request.go
new file mode 100644
index 0000000..6c708c0
--- /dev/null
+++ b/gopher/request.go
@@ -0,0 +1,72 @@
+package gopher
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "net/url"
+ "path"
+ "strings"
+
+ "tildegit.org/tjp/gus"
+)
+
+// ParseRequest parses a gopher protocol request into a gus.Request object.
+func ParseRequest(rdr io.Reader) (*gus.Request, error) {
+ selector, search, err := readFullRequest(rdr)
+ if err != nil {
+ return nil, err
+ }
+
+ if !strings.HasPrefix(selector, "/") {
+ selector = "/" + selector
+ }
+
+ return &gus.Request{
+ URL: &url.URL{
+ Scheme: "gopher",
+ Path: path.Clean(strings.TrimRight(selector, "\r\n")),
+ OmitHost: true, //nolint:typecheck
+ // (for some reason typecheck on drone-ci doesn't realize OmitHost is a field in url.URL)
+ RawQuery: url.QueryEscape(strings.TrimRight(search, "\r\n")),
+ },
+ }, 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 string(selector), string(search), nil
+}