summaryrefslogtreecommitdiff
path: root/gopher/response.go
blob: 566623f89d2bdaa517c1f7db571b0e222da33111 (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"

	sr "tildegit.org/tjp/sliderule"
)

// The Canonical gopher item types.
const (
	TextFileType      sr.Status = '0'
	MenuType          sr.Status = '1'
	CSOPhoneBookType  sr.Status = '2'
	ErrorType         sr.Status = '3'
	MacBinHexType     sr.Status = '4'
	DosBinType        sr.Status = '5'
	UuencodedType     sr.Status = '6'
	SearchType        sr.Status = '7'
	TelnetSessionType sr.Status = '8'
	BinaryFileType    sr.Status = '9'
	MirrorServerType  sr.Status = '+'
	GifFileType       sr.Status = 'g'
	ImageFileType     sr.Status = 'I'
	Telnet3270Type    sr.Status = 'T'
)

// The gopher+ types.
const (
	BitmapType    sr.Status = ':'
	MovieFileType sr.Status = ';'
	SoundFileType sr.Status = '<'
)

// The various non-canonical gopher types.
const (
	DocumentType     sr.Status = 'd'
	HTMLType         sr.Status = 'h'
	InfoMessageType  sr.Status = 'i'
	PngImageFileType sr.Status = 'p'
	RtfDocumentType  sr.Status = 'r'
	WavSoundFileType sr.Status = 's'
	PdfDocumentType  sr.Status = 'P'
	XmlDocumentType  sr.Status = 'X'
)

// MapItem is a single item in a gophermap.
type MapItem struct {
	Type     sr.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() *sr.Response {
	return &sr.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() *sr.Response {
	return &sr.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 sr.Status, contents io.Reader) *sr.Response {
	return &sr.Response{Status: status, Body: contents}
}

// NewResponseReader produces a reader which supports reading gopher protocol responses.
func NewResponseReader(response *sr.Response) sr.ResponseReader {
	return &responseReader{
		Response: response,
		once:     &sync.Once{},
	}
}

type responseReader struct {
	*sr.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)
	})
}