summaryrefslogtreecommitdiff
path: root/gopher/response.go
blob: c600b107383206d83f3de71586cb9ac308e7ec95 (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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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)
	})
}