summaryrefslogtreecommitdiff
path: root/gopher/response.go
blob: 1ad7f1dcc80d05769faa5cdc9661dd014f9404ff (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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package gopher

import (
	"bytes"
	"fmt"
	"io"
	"mime"
	"path"
	"strings"
	"sync"

	"tildegit.org/tjp/sliderule/internal/types"
)

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

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

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

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

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

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

// GuessItemType attempts to find the best gopher item type for a file based on its name.
func GuessItemType(filepath string) types.Status {
	ext := path.Ext(filepath)
	switch ext {
	case ".gophermap":
		return MenuType
	case ".txt", ".gmi", ".md":
		return TextFileType
	case ".gif":
		return  GifFileType
	case ".png":
		return PngImageFileType
	case ".jpg", ".jpeg", ".tif", ".tiff":
		return ImageFileType
	case ".mp4", ".mov":
		return MovieFileType
	case ".pcm", ".mp3", ".aiff", ".aif", ".aac", ".ogg", ".flac", ".alac", ".wma":
		return SoundFileType
	case ".bmp":
		return BitmapType
	case ".doc", ".docx", ".odt", ".fodt":
		return DocumentType
	case ".html", ".htm":
		return HTMLType
	case ".rtf":
		return RtfDocumentType
	case ".wav":
		return WavSoundFileType
	case ".pdf":
		return PdfDocumentType
	case ".xml", ".atom":
		return XmlDocumentType
	case ".exe", ".bin", ".out", ".dylib", ".dll", ".so", ".a", ".o":
		return BinaryFileType
	}

	mtype := mime.TypeByExtension(ext)
	if strings.HasPrefix(mtype, "text/") {
		return TextFileType
	}

	return BinaryFileType
}