DEV Community

loading...

A Gemini Client in Rust - 12 Handling Mime Types

Nivethan
Originally published at nivethan.dev Updated on ・4 min read

Hello! We are now almost done. We just have one piece left to implement. Currently we have hardcoded that all responses we get from Gemini server are text/gemini.

In this chapter we are going parse the Gemini response properly and then based on mime type, do different things. If the mime type is text/gemini we are going to do what we currently do which is parse the page and display links properly.

If the mime type is text/*, we are going to just display the text on the screen with no formatting.

Anything else and we will save the the contents of that file to the disk and leave it up to the user to deal with.

Let's get started!

Handling Mime Types

We have a few different places we can handle mime types but the place that makes the most sense to me is in our visit function.

...
fn visit(url: &Url) -> String {
...
        '2' => {
            if response.body == None {
                client.read_tls(&mut socket).unwrap();
                client.process_new_packets().unwrap();
                let mut data = Vec::new();
                let _ = client.read_to_end(&mut data);

                response.body =  Some(String::from_utf8_lossy(&data).to_string());
            }

            let mime_type = response.mime_type;

            if mime_type != None {
                let mime = mime_type.unwrap();

                match  mime.as_str() {
                    "text/gemini" => response.body.unwrap_or("".to_string()),
                    _ if mime.starts_with("text/") => {
                        println!("{}", response.body.unwrap());
                        format!("Requested page was {}.", mime)
                    },
                    _ => {
                        let file_name = format!("/home/nivethan/gemini/unknown/{}.unknown", 
                            url.request().trim().replace("/", "-"));
                        let mut f = File::create(&file_name).unwrap();
                        f.write(response.body.unwrap().as_bytes()).unwrap();
                        println!("Saved {}!", file_name);
                        format!("Requested page was {}.", mime)
                    }
                }

            } else {
                response.body.unwrap_or("".to_string())
            }
        },
...
Enter fullscreen mode Exit fullscreen mode

In our visit function, we had set it so that on success we poll the TLS session for any data if we're missing the response body. Once we have the response.body, we return that back to wherever we called our visit function.

Now we are going to check the mimetype instead of immediately passing the data back. If the mimetype is anything other than text/gemini, we are going to send back a message explaining that the request wasn't a gemini request and that we did something else.

The first thing we do is make sure our mime type isn't None. If it is we assume the mime type is text/gemini. This is part of the gemini specification.

Next we match mime against the various types.

First we match against text/gemini, here we return the response.body so that the page can be parsed later on.

Next we match against text/*, any mime type starting with text/ will be printed directly to the screen. We send back a message letting the user know that we did something else. This message will now appear when the user enters ls or more.

The catch all case is that that we save the data directly to the hard drive. We save this with an extension, .unknown. We could map out the various mime types to their extensions but we'll stick with this to keep things simple.

With that we have the various mime types being handled! Not well but handled!

Now let's parse the Status!

Parsing the Status

Now that we have the handling of mime types done, we just need to parse the status information into the Response object.

...
impl Response {
    fn new(data: String) -> Self {
        let tokens: Vec<&str> = data.splitn(2, "\r\n").collect();
        let status = Status::new(tokens[0].to_string());

        match status.code.chars().next().unwrap() {
            '2' => {
                let meta_tokens: Vec<&str> = status.meta.split(";").collect();

                let mut mime_type = None;
                let mut charset = None;
                let mut lang = None;

                for meta_token in meta_tokens {
                    if meta_token.contains("/") {
                        mime_type = Some(meta_token.trim().to_string());
                    }
                    if meta_token.contains("charset") {
                        charset = Some(meta_token.trim().to_string());
                    }
                    if meta_token.contains("lang") {
                        lang = Some(meta_token.trim().to_string());
                    }
                }

                let body;
                if tokens[1] != "" {
                    body = Some(tokens[1].to_string());
                } else {
                    body = None;
                }

                Response { status, mime_type, charset, lang, body }
            },
            _ => {
                Response { status, mime_type: None, charset: None, lang: None, body: None }
            }
        }
    }
}
...
Enter fullscreen mode Exit fullscreen mode

The key part is that the meta field can have 3 parts to it, separated by a semi-colon. We can have the mime type, charset and lang parameter. We can also have none of these!

We initialize these attributes to None first and we split the meta field on the semi-colon.

We loop through the meta_tokens and we check to see if we have the mime type, charset or lang parameter. If we do, we update our variables.

Voila! We have processed the Gemini meta line now.

! We can test everything by hard coding in a mime type and changing the status.meta.split() call to our hard coded mime type.

...
                let mime = "text/";
                let meta_tokens: Vec<&str> = mime.split(";").collect();
...
Enter fullscreen mode Exit fullscreen mode

We should see our gemini responses being printed directly to the screen!

With that we are now done our basic client!

We have a Gemini client that can visit Gemini servers, display them properly, follow links, bookmark pages, save pages and handle the various mime types!

Whew! That was a trek, in the next chapter let's debrief.

See you soon!

Discussion (0)