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
// Copyright 2018-2020 Sebastian Wiesner <sebastian@swsnr.de>

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//! File resources.

use std::fs::File;
use std::io::prelude::*;
use std::io::{Error, ErrorKind, Result};

use tracing::{event, instrument, Level};
use url::Url;

use super::{filter_schemes, MimeData, ResourceUrlHandler};

/// A resource handler for `file:` URLs.
#[derive(Debug, Clone)]
pub struct FileResourceHandler {
    read_limit: u64,
}

impl FileResourceHandler {
    /// Create a resource handler for `file:` URLs.
    ///
    /// The resource handler does not read beyond `read_limit`.
    pub fn new(read_limit: u64) -> Self {
        Self { read_limit }
    }
}

impl ResourceUrlHandler for FileResourceHandler {
    #[instrument(level = "debug", skip(self))]
    fn read_resource(&self, url: &Url) -> Result<MimeData> {
        filter_schemes(&["file"], url).and_then(|url| {
            match url.to_file_path() {
                Ok(path) => {
                    event!(
                        Level::DEBUG,
                        "Reading from resource file {}",
                        path.display()
                    );
                    let mut buffer = Vec::new();
                    File::open(&path)?
                        // Read a byte more than the limit differentiate an expected EOF from hitting the limit
                        .take(self.read_limit + 1)
                        .read_to_end(&mut buffer)?;

                    if self.read_limit < buffer.len() as u64 {
                        Err(Error::new(
                            ErrorKind::InvalidData,
                            // TODO: Use ErrorKind::FileTooLarge once stabilized
                            format!("Contents of {url} exceeded {} bytes", self.read_limit),
                        ))
                    } else {
                        let mime_type = mime_guess::from_path(&path).first();
                        if mime_type.is_none() {
                            event!(
                                Level::DEBUG,
                                "Failed to guess mime type from {}",
                                path.display()
                            );
                        }
                        Ok(MimeData {
                            mime_type,
                            data: buffer,
                        })
                    }
                }
                Err(_) => Err(Error::new(
                    ErrorKind::InvalidInput,
                    format!("Cannot convert URL {url} to file path"),
                )),
            }
        })
    }
}

#[cfg(test)]
mod tests {
    use crate::resources::*;
    use similar_asserts::assert_eq;
    use url::Url;

    #[test]
    fn read_resource_returns_content_type() {
        let cwd = Url::from_directory_path(std::env::current_dir().unwrap()).unwrap();
        let client = FileResourceHandler::new(5_000_000);

        let resource = cwd.join("../sample/rust-logo.svg").unwrap();
        let mime_type = client.read_resource(&resource).unwrap().mime_type;
        assert_eq!(mime_type, Some(mime::IMAGE_SVG));

        let resource = cwd.join("../sample/rust-logo-128x128.png").unwrap();
        let mime_type = client.read_resource(&resource).unwrap().mime_type;
        assert_eq!(mime_type, Some(mime::IMAGE_PNG));
    }

    #[test]
    fn read_resource_obeys_size_limit() {
        let cwd = Url::from_directory_path(std::env::current_dir().unwrap()).unwrap();
        let client = FileResourceHandler { read_limit: 10 };

        let resource = cwd.join("../sample/rust-logo.svg").unwrap();
        let error = client.read_resource(&resource).unwrap_err().to_string();
        assert_eq!(error, format!("Contents of {resource} exceeded 10 bytes"));
    }

    #[test]
    fn read_resource_ignores_http() {
        let url = Url::parse("https://example.com").unwrap();

        let client = FileResourceHandler { read_limit: 10 };
        let error = client.read_resource(&url).unwrap_err().to_string();
        assert_eq!(
            error,
            "Unsupported scheme in https://example.com/, expected one of [\"file\"]"
        );
    }
}