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.TrimSuffix(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.TrimSuffix(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 }