net/
local_directory_listing.rs1use 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 return Response::network_error(NetworkError::LocalDirectoryError);
23 }
24
25 if !request.origin.is_opaque() {
26 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
50pub(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 let name = script_string_literal(&name);
136 output.push_str(&format!(
137 "[{class:?}, {name}, {file_url_string:?}, {file_size:?}, {last_modified:?}],"
138 ));
139}
140
141fn script_string_literal(value: &str) -> String {
146 format!("{value:?}").replace('<', "\\u003C")
147}
148
149pub fn metadata_to_file_size_string(metadata: &Metadata) -> String {
150 if !metadata.is_file() {
151 return String::new();
152 }
153
154 let mut float_size = metadata.len() as f64;
155 let mut prefix_power = 0;
156 while float_size > 1000.0 && prefix_power < 3 {
157 float_size /= 1000.0;
158 prefix_power += 1;
159 }
160
161 let prefix = match prefix_power {
162 0 => "B",
163 1 => "KB",
164 2 => "MB",
165 _ => "GB",
166 };
167
168 format!("{:.2} {prefix}", float_size)
169}
170
171#[cfg(test)]
172mod tests {
173 use super::script_string_literal;
174
175 #[test]
176 fn script_string_literal_neutralises_script_close() {
177 let literal = script_string_literal("a</script><img src=x onerror=alert(1)>b");
178 assert!(!literal.contains('<'));
179 assert!(!literal.to_lowercase().contains("</script"));
180 }
181
182 #[test]
183 fn script_string_literal_keeps_debug_escaping() {
184 assert_eq!(script_string_literal("a\"b\\c"), r#""a\"b\\c""#);
187 assert_eq!(script_string_literal("plain"), r#""plain""#);
188 }
189}