summaryrefslogtreecommitdiff
path: root/gopher/response.go
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-01-28 14:52:35 -0700
committertjpcc <tjp@ctrl-c.club>2023-01-28 15:01:41 -0700
commit66a1b1f39a1e1d5499b548b36d18c8daa872d7da (patch)
tree96471dbd5486ede1a908790ac23e0c55b226dfad /gopher/response.go
parenta27b879accb191b6a6c6e76a6251ed751967f73a (diff)
gopher support.
Some of the contrib packages were originally built gemini-specific and had to be refactored into generic core functionality and thin protocol-specific wrappers for each of gemini and gopher.
Diffstat (limited to 'gopher/response.go')
-rw-r--r--gopher/response.go162
1 files changed, 162 insertions, 0 deletions
diff --git a/gopher/response.go b/gopher/response.go
new file mode 100644
index 0000000..c600b10
--- /dev/null
+++ b/gopher/response.go
@@ -0,0 +1,162 @@
+package gopher
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "sync"
+
+ "tildegit.org/tjp/gus"
+)
+
+// The Canonical gopher item types.
+const (
+ TextFileType gus.Status = '0'
+ MenuType gus.Status = '1'
+ CSOPhoneBookType gus.Status = '2'
+ ErrorType gus.Status = '3'
+ MacBinHexType gus.Status = '4'
+ DosBinType gus.Status = '5'
+ UuencodedType gus.Status = '6'
+ SearchType gus.Status = '7'
+ TelnetSessionType gus.Status = '8'
+ BinaryFileType gus.Status = '9'
+ MirrorServerType gus.Status = '+'
+ GifFileType gus.Status = 'g'
+ ImageFileType gus.Status = 'I'
+ Telnet3270Type gus.Status = 'T'
+)
+
+// The gopher+ types.
+const (
+ BitmapType gus.Status = ':'
+ MovieFileType gus.Status = ';'
+ SoundFileType gus.Status = '<'
+)
+
+// The various non-canonical gopher types.
+const (
+ DocumentType gus.Status = 'd'
+ HTMLType gus.Status = 'h'
+ InfoMessageType gus.Status = 'i'
+ PngImageFileType gus.Status = 'p'
+ RtfDocumentType gus.Status = 'r'
+ WavSoundFileType gus.Status = 's'
+ PdfDocumentType gus.Status = 'P'
+ XmlDocumentType gus.Status = 'X'
+)
+
+// MapItem is a single item in a gophermap.
+type MapItem struct {
+ Type gus.Status
+ Display string
+ Selector string
+ Hostname string
+ Port string
+}
+
+// String serializes the item into a gophermap CRLF-terminated text line.
+func (mi MapItem) String() string {
+ return fmt.Sprintf(
+ "%s%s\t%s\t%s\t%s\r\n",
+ []byte{byte(mi.Type)},
+ mi.Display,
+ mi.Selector,
+ mi.Hostname,
+ mi.Port,
+ )
+}
+
+// Response builds a response which contains just this single MapItem.
+//
+// Meta in the response will be a pointer to the MapItem.
+func (mi *MapItem) Response() *gus.Response {
+ return &gus.Response{
+ Status: mi.Type,
+ Meta: &mi,
+ Body: bytes.NewBufferString(mi.String() + ".\r\n"),
+ }
+}
+
+// MapDocument is a list of map items which can print out a full gophermap document.
+type MapDocument []MapItem
+
+// String serializes the document into gophermap format.
+func (md MapDocument) String() string {
+ return md.serialize().String()
+}
+
+// Response builds a gopher response containing the gophermap.
+//
+// Meta will be the MapDocument itself.
+func (md MapDocument) Response() *gus.Response {
+ return &gus.Response{
+ Status: DocumentType,
+ Meta: md,
+ Body: md.serialize(),
+ }
+}
+
+func (md MapDocument) serialize() *bytes.Buffer {
+ buf := &bytes.Buffer{}
+ for _, mi := range md {
+ _, _ = buf.WriteString(mi.String())
+ }
+ _, _ = buf.WriteString(".\r\n")
+ return buf
+}
+
+// Error builds an error message MapItem.
+func Error(err error) *MapItem {
+ return &MapItem{
+ Type: ErrorType,
+ Display: err.Error(),
+ Hostname: "none",
+ Port: "0",
+ }
+}
+
+// File builds a minimal response delivering a file's contents.
+//
+// Meta is nil and Status is 0 in this response.
+func File(status gus.Status, contents io.Reader) *gus.Response {
+ return &gus.Response{Status: status, Body: contents}
+}
+
+// NewResponseReader produces a reader which supports reading gopher protocol responses.
+func NewResponseReader(response *gus.Response) gus.ResponseReader {
+ return &responseReader{
+ Response: response,
+ once: &sync.Once{},
+ }
+}
+
+type responseReader struct {
+ *gus.Response
+ reader io.Reader
+ once *sync.Once
+}
+
+func (rdr *responseReader) Read(b []byte) (int, error) {
+ rdr.ensureReader()
+ return rdr.reader.Read(b)
+}
+
+func (rdr *responseReader) WriteTo(dst io.Writer) (int64, error) {
+ rdr.ensureReader()
+ return rdr.reader.(io.WriterTo).WriteTo(dst)
+}
+
+func (rdr *responseReader) ensureReader() {
+ rdr.once.Do(func() {
+ if _, ok := rdr.Body.(io.WriterTo); ok {
+ rdr.reader = rdr.Body
+ return
+ }
+
+ // rdr.reader needs to implement WriterTo, so in this case
+ // we borrow an implementation in terms of io.Reader from
+ // io.MultiReader.
+ rdr.reader = io.MultiReader(rdr.Body)
+ })
+}