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 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", ".gph":
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
}
|