pulldown_cmark_mdcat/resources/
file.rs

1// Copyright 2018-2020 Sebastian Wiesner <sebastian@swsnr.de>
2
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! File resources.
8
9use std::fs::File;
10use std::io::prelude::*;
11use std::io::{Error, ErrorKind, Result};
12use std::path::Path;
13
14use mime::Mime;
15use tracing::{event, instrument, Level};
16use url::Url;
17
18use super::{filter_schemes, MimeData, ResourceUrlHandler};
19
20/// A resource handler for `file:` URLs.
21#[derive(Debug, Clone)]
22pub struct FileResourceHandler {
23    read_limit: u64,
24}
25
26impl FileResourceHandler {
27    /// Create a resource handler for `file:` URLs.
28    ///
29    /// The resource handler does not read beyond `read_limit`.
30    pub fn new(read_limit: u64) -> Self {
31        Self { read_limit }
32    }
33}
34
35/// Guess a mimetype, in so far as mdcat makes use of the mime type.
36///
37/// This function recognizes
38///
39/// - SVG images because mdcat needs to render SVG images explicitly, and
40/// - PNG images because kitty can pass through PNG images in some cases.
41///
42/// It checks mime types exclusively by looking at the lowercase extension.
43///
44/// It ignores all other extensions and mime types and returns `None` in these cases.
45fn guess_mimetype<P: AsRef<Path>>(path: P) -> Option<Mime> {
46    path.as_ref()
47        .extension()
48        .map(|s| s.to_ascii_lowercase())
49        .and_then(|s| match s.to_str() {
50            Some("png") => Some(mime::IMAGE_PNG),
51            Some("svg") => Some(mime::IMAGE_SVG),
52            _ => None,
53        })
54}
55
56impl ResourceUrlHandler for FileResourceHandler {
57    #[instrument(level = "debug", skip(self))]
58    fn read_resource(&self, url: &Url) -> Result<MimeData> {
59        filter_schemes(&["file"], url).and_then(|url| {
60            match url.to_file_path() {
61                Ok(path) => {
62                    event!(
63                        Level::DEBUG,
64                        "Reading from resource file {}",
65                        path.display()
66                    );
67                    let mut buffer = Vec::new();
68                    File::open(&path)?
69                        // Read a byte more than the limit differentiate an expected EOF from hitting the limit
70                        .take(self.read_limit + 1)
71                        .read_to_end(&mut buffer)?;
72
73                    if self.read_limit < buffer.len() as u64 {
74                        Err(Error::new(
75                            ErrorKind::FileTooLarge,
76                            format!("Contents of {url} exceeded {} bytes", self.read_limit),
77                        ))
78                    } else {
79                        let mime_type = guess_mimetype(&path);
80                        if mime_type.is_none() {
81                            event!(
82                                Level::DEBUG,
83                                "Failed to guess mime type from {}",
84                                path.display()
85                            );
86                        }
87                        Ok(MimeData {
88                            mime_type,
89                            data: buffer,
90                        })
91                    }
92                }
93                Err(_) => Err(Error::new(
94                    ErrorKind::InvalidInput,
95                    format!("Cannot convert URL {url} to file path"),
96                )),
97            }
98        })
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use crate::resources::*;
105    use similar_asserts::assert_eq;
106    use url::Url;
107
108    #[test]
109    fn read_resource_returns_content_type() {
110        let cwd = Url::from_directory_path(std::env::current_dir().unwrap()).unwrap();
111        let client = FileResourceHandler::new(5_000_000);
112
113        let resource = cwd.join("../sample/rust-logo.svg").unwrap();
114        let mime_type = client.read_resource(&resource).unwrap().mime_type;
115        assert_eq!(mime_type, Some(mime::IMAGE_SVG));
116
117        let resource = cwd.join("../sample/rust-logo-128x128.png").unwrap();
118        let mime_type = client.read_resource(&resource).unwrap().mime_type;
119        assert_eq!(mime_type, Some(mime::IMAGE_PNG));
120    }
121
122    #[test]
123    fn read_resource_obeys_size_limit() {
124        let cwd = Url::from_directory_path(std::env::current_dir().unwrap()).unwrap();
125        let client = FileResourceHandler { read_limit: 10 };
126
127        let resource = cwd.join("../sample/rust-logo.svg").unwrap();
128        let error = client.read_resource(&resource).unwrap_err().to_string();
129        assert_eq!(error, format!("Contents of {resource} exceeded 10 bytes"));
130    }
131
132    #[test]
133    fn read_resource_ignores_http() {
134        let url = Url::parse("https://example.com").unwrap();
135
136        let client = FileResourceHandler { read_limit: 10 };
137        let error = client.read_resource(&url).unwrap_err().to_string();
138        assert_eq!(
139            error,
140            "Unsupported scheme in https://example.com/, expected one of [\"file\"]"
141        );
142    }
143}