net/
local_directory_listing.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::fs::Metadata;
6use std::path::PathBuf;
7
8use chrono::{DateTime, Local};
9use embedder_traits::resources::{Resource, read_string};
10use headers::{ContentType, HeaderMapExt};
11use net_traits::request::Request;
12use net_traits::response::{Response, ResponseBody};
13use net_traits::{NetworkError, ResourceFetchTiming};
14use servo_config::pref;
15use servo_url::ServoUrl;
16use url::Url;
17
18pub(crate) async fn fetch(request: &mut Request, url: ServoUrl, path_buf: PathBuf) -> Response {
19    if !pref!(network_local_directory_listing_enabled) {
20        // If you want to be able to browse local directories, configure Servo prefs so that
21        // "network.local_directory_listing.enabled" is set to true.
22        return Response::network_error(NetworkError::LocalDirectoryError);
23    }
24
25    if !request.origin.is_opaque() {
26        // Checking for an opaque origin as a shorthand for user activation
27        // as opposed to a request originating from a script.
28        // TODO(32534): carefully consider security of this approach.
29        return Response::network_error(NetworkError::LocalDirectoryError);
30    }
31
32    let directory_contents = match tokio::fs::read_dir(path_buf.clone()).await {
33        Ok(directory_contents) => directory_contents,
34        Err(error) => {
35            return Response::network_error(NetworkError::ResourceLoadError(format!(
36                "Unable to access directory: {error}"
37            )));
38        },
39    };
40
41    let output = build_html_directory_listing(url.as_url(), path_buf, directory_contents).await;
42
43    let mut response = Response::new(url, ResourceFetchTiming::new(request.timing_type()));
44    response.headers.typed_insert(ContentType::html());
45    *response.body.lock() = ResponseBody::Done(output.into_bytes());
46
47    response
48}
49
50/// Returns an the string of an JavaScript `<script>` tag calling the `setData` function with the
51/// contents of the given [`ReadDir`] directory listing.
52///
53/// # Arguments
54///
55/// * `url` - the original URL of the request that triggered this directory listing.
56/// * `path` - the full path to the local directory.
57/// * `directory_contents` - a [`ReadDir`] with the contents of the directory.
58pub(crate) async fn build_html_directory_listing(
59    url: &Url,
60    path: PathBuf,
61    mut directory_contents: tokio::fs::ReadDir,
62) -> String {
63    let mut page_html = String::with_capacity(1024);
64    page_html.push_str("<!DOCTYPE html>");
65
66    let mut parent_url_string = String::new();
67    if path.parent().is_some() {
68        let mut parent_url = url.clone();
69        if let Ok(mut path_segments) = parent_url.path_segments_mut() {
70            path_segments.pop();
71        }
72        parent_url.as_str().clone_into(&mut parent_url_string);
73    }
74
75    page_html.push_str(&read_string(Resource::DirectoryListingHTML));
76
77    page_html.push_str("<script>\n");
78    page_html.push_str(&format!(
79        "setData({:?}, {:?}, [",
80        url.as_str(),
81        parent_url_string
82    ));
83
84    while let Ok(Some(directory_entry)) = directory_contents.next_entry().await {
85        let Ok(metadata) = directory_entry.metadata().await else {
86            continue;
87        };
88        write_directory_entry(directory_entry, metadata, url, &mut page_html);
89    }
90
91    page_html.push_str("]);");
92    page_html.push_str("</script>\n");
93
94    page_html
95}
96
97fn write_directory_entry(
98    entry: tokio::fs::DirEntry,
99    metadata: Metadata,
100    url: &Url,
101    output: &mut String,
102) {
103    let Ok(name) = entry.file_name().into_string() else {
104        return;
105    };
106
107    let mut file_url = url.clone();
108    {
109        let Ok(mut path_segments) = file_url.path_segments_mut() else {
110            return;
111        };
112        path_segments.push(&name);
113    }
114
115    let class = if metadata.is_dir() {
116        "directory"
117    } else if metadata.is_symlink() {
118        "symlink"
119    } else {
120        "file"
121    };
122
123    let file_url_string = &file_url.to_string();
124    let file_size = metadata_to_file_size_string(&metadata);
125    let last_modified = metadata
126        .modified()
127        .map(DateTime::<Local>::from)
128        .map(|time| time.format("%F %r").to_string())
129        .unwrap_or_default();
130
131    output.push_str(&format!(
132        "[{class:?}, {name:?}, {file_url_string:?}, {file_size:?}, {last_modified:?}],"
133    ));
134}
135
136pub fn metadata_to_file_size_string(metadata: &Metadata) -> String {
137    if !metadata.is_file() {
138        return String::new();
139    }
140
141    let mut float_size = metadata.len() as f64;
142    let mut prefix_power = 0;
143    while float_size > 1000.0 && prefix_power < 3 {
144        float_size /= 1000.0;
145        prefix_power += 1;
146    }
147
148    let prefix = match prefix_power {
149        0 => "B",
150        1 => "KB",
151        2 => "MB",
152        _ => "GB",
153    };
154
155    format!("{:.2} {prefix}", float_size)
156}