gophermap/
lib.rs

1//! gophermap is a Rust crate that can parse and generate Gopher responses.
2//! It can be used to implement Gopher clients and servers. It doesn't handle
3//! any I/O on purpose. This library is meant to be used by other servers and
4//! clients in order to avoid re-implementing the gophermap logic.
5//!
6use std::io::Write;
7
8/// A single entry in a Gopher map. This struct can be filled in order to
9/// generate Gopher responses. It can also be the result of parsing one.
10pub struct GopherEntry<'a> {
11    /// The type of the link
12    pub item_type: ItemType,
13    /// The human-readable description of the link. Displayed on the UI.
14    pub display_string: &'a str,
15    /// The target page (selector) of the link
16    pub selector: &'a str,
17    /// The host for the target of the link
18    pub host: &'a str,
19    /// The port for the target of the link
20    pub port: u16,
21}
22
23impl<'a> GopherEntry<'a> {
24    /// Parse a line into a Gopher directory entry.
25    /// ```rust
26    /// use gophermap::GopherEntry;
27    /// let entry = GopherEntry::from("1Floodgap Home	/home	gopher.floodgap.com	70\r\n")
28    ///     .unwrap();
29    /// assert_eq!(entry.selector, "/home");
30    /// ```
31    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    /// Serializes a Gopher entry into bytes. This function can be used to
56    /// generate Gopher responses.
57    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/// Item type for a Gopher directory entry
117#[derive(Debug, PartialEq)]
118pub enum ItemType {
119    /// Item is a file
120    File,
121    /// Item is a directory
122    Directory,
123    /// Item is a CSO phone-book server
124    CsoServer,
125    /// Error
126    Error,
127    /// Item is a BinHexed Macintosh file.
128    BinHex,
129    /// Item is a DOS binary archive of some sort.
130    /// Client must read until the TCP connection closes. Beware.
131    DosBinary,
132    /// Item is a UNIX uuencoded file.
133    Uuencoded,
134    /// Item is an Index-Search server.
135    Search,
136    /// Item points to a text-based telnet session.
137    Telnet,
138    /// Item is a binary file!
139    /// Client must read until the TCP connection closes. Beware.
140    Binary,
141    /// Item is a redundant server
142    RedundantServer,
143    /// Item points to a text-based tn3270 session.
144    Tn3270,
145    /// Item is a GIF format graphics file.
146    Gif,
147    /// Item is some sort of image file. Client decides how to display.
148    Image,
149    /// Informational message
150    Info,
151    /// Other types
152    Other(char),
153}
154
155impl ItemType {
156    /// Parses a char into an Item Type
157    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    /// Turns an Item Type into a char
179    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}