1use std::io::Write;
7
8pub struct GopherEntry<'a> {
11 pub item_type: ItemType,
13 pub display_string: &'a str,
15 pub selector: &'a str,
17 pub host: &'a str,
19 pub port: u16,
21}
22
23impl<'a> GopherEntry<'a> {
24 pub fn from(line: &'a str) -> Option<Self> {
32 let line = {
33 let mut chars = line.chars();
34 if !(chars.next_back()? == '\n' && chars.next_back()? == '\r') {
35 return None;
36 }
37 chars.as_str()
38 };
39
40 let mut parts = line.split('\t');
41
42 Some(GopherEntry {
43 item_type: ItemType::from(line.chars().next()?),
44 display_string: {
45 let part = parts.next()?;
46 let (index, _) = part.char_indices().skip(1).next()?;
47 &part[index..]
48 },
49 selector: parts.next()?,
50 host: parts.next()?,
51 port: parts.next()?.parse().ok()?,
52 })
53 }
54
55 pub fn write<W>(&self, mut buf: W) -> std::io::Result<()>
58 where
59 W: Write,
60 {
61 write!(
62 buf,
63 "{}{}\t{}\t{}\t{}\r\n",
64 self.item_type.to_char(),
65 self.display_string,
66 self.selector,
67 self.host,
68 self.port
69 )?;
70 Ok(())
71 }
72}
73
74pub struct GopherMenu<W>
75where
76 W: Write,
77{
78 target: W,
79}
80
81impl<'a, W> GopherMenu<&'a W>
82where
83 &'a W: Write,
84{
85 pub fn with_write(target: &'a W) -> Self {
86 GopherMenu { target: &target }
87 }
88
89 pub fn info(&self, text: &str) -> std::io::Result<()> {
90 self.write_entry(ItemType::Info, text, "FAKE", "fake.host", 1)
91 }
92
93 pub fn write_entry(
94 &self,
95 item_type: ItemType,
96 text: &str,
97 selector: &str,
98 host: &str,
99 port: u16,
100 ) -> std::io::Result<()> {
101 GopherEntry {
102 item_type,
103 display_string: text,
104 selector,
105 host,
106 port,
107 }
108 .write(self.target)
109 }
110
111 pub fn end(&mut self) -> std::io::Result<()> {
112 write!(self.target, ".\r\n")
113 }
114}
115
116#[derive(Debug, PartialEq)]
118pub enum ItemType {
119 File,
121 Directory,
123 CsoServer,
125 Error,
127 BinHex,
129 DosBinary,
132 Uuencoded,
134 Search,
136 Telnet,
138 Binary,
141 RedundantServer,
143 Tn3270,
145 Gif,
147 Image,
149 Info,
151 Other(char),
153}
154
155impl ItemType {
156 pub fn from(c: char) -> Self {
158 match c {
159 '0' => ItemType::File,
160 '1' => ItemType::Directory,
161 '2' => ItemType::CsoServer,
162 '3' => ItemType::Error,
163 '4' => ItemType::BinHex,
164 '5' => ItemType::DosBinary,
165 '6' => ItemType::Uuencoded,
166 '7' => ItemType::Search,
167 '8' => ItemType::Telnet,
168 '9' => ItemType::Binary,
169 '+' => ItemType::RedundantServer,
170 'T' => ItemType::Tn3270,
171 'g' => ItemType::Gif,
172 'I' => ItemType::Image,
173 'i' => ItemType::Info,
174 c => ItemType::Other(c),
175 }
176 }
177
178 pub fn to_char(&self) -> char {
180 match self {
181 ItemType::File => '0',
182 ItemType::Directory => '1',
183 ItemType::CsoServer => '2',
184 ItemType::Error => '3',
185 ItemType::BinHex => '4',
186 ItemType::DosBinary => '5',
187 ItemType::Uuencoded => '6',
188 ItemType::Search => '7',
189 ItemType::Telnet => '8',
190 ItemType::Binary => '9',
191 ItemType::RedundantServer => '+',
192 ItemType::Tn3270 => 'T',
193 ItemType::Gif => 'g',
194 ItemType::Image => 'I',
195 ItemType::Info => 'i',
196 ItemType::Other(c) => *c,
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 fn get_test_pairs() -> Vec<(String, GopherEntry<'static>)> {
206 let mut pairs = Vec::new();
207
208 pairs.push((
209 "1Floodgap Home /home gopher.floodgap.com 70\r\n".to_owned(),
210 GopherEntry {
211 item_type: ItemType::Directory,
212 display_string: "Floodgap Home",
213 selector: "/home",
214 host: "gopher.floodgap.com",
215 port: 70,
216 },
217 ));
218
219 pairs.push((
220 "iWelcome to my page FAKE (NULL) 0\r\n".to_owned(),
221 GopherEntry {
222 item_type: ItemType::Info,
223 display_string: "Welcome to my page",
224 selector: "FAKE",
225 host: "(NULL)",
226 port: 0,
227 },
228 ));
229
230 return pairs;
231 }
232
233 #[test]
234 fn test_parse() {
235 for (raw, parsed) in get_test_pairs() {
236 let entry = GopherEntry::from(&raw).unwrap();
237 assert_eq!(entry.item_type, parsed.item_type);
238 assert_eq!(entry.display_string, parsed.display_string);
239 assert_eq!(entry.selector, parsed.selector);
240 assert_eq!(entry.host, parsed.host);
241 assert_eq!(entry.port, parsed.port);
242 }
243 }
244
245 #[test]
246 fn test_write() {
247 for (raw, parsed) in get_test_pairs() {
248 let mut output = Vec::new();
249 parsed.write(&mut output).unwrap();
250 let line = String::from_utf8(output).unwrap();
251 assert_eq!(raw, line);
252 }
253 }
254}