net/
local_directory_listing.rs1use std::fmt::Write as _;
6use std::fs::Metadata;
7use std::path::PathBuf;
8
9use chrono::{DateTime, Local};
10use embedder_traits::resources::{Resource, read_string};
11use headers::{ContentType, HeaderMapExt};
12use net_traits::request::Request;
13use net_traits::response::{Response, ResponseBody};
14use net_traits::{NetworkError, ResourceFetchTiming};
15use servo_config::pref;
16use servo_url::ServoUrl;
17use url::Url;
18
19pub(crate) async fn fetch(request: &mut Request, url: ServoUrl, path_buf: PathBuf) -> Response {
20 if !pref!(network_local_directory_listing_enabled) {
21 return Response::network_error(NetworkError::LocalDirectoryError);
24 }
25
26 if !request.origin.is_opaque() {
27 return Response::network_error(NetworkError::LocalDirectoryError);
31 }
32
33 let directory_contents = match tokio::fs::read_dir(&path_buf).await {
34 Ok(directory_contents) => directory_contents,
35 Err(error) => {
36 return Response::network_error(NetworkError::ResourceLoadError(format!(
37 "Unable to access directory: {error}"
38 )));
39 },
40 };
41
42 let output = build_html_directory_listing(url.as_url(), path_buf, directory_contents).await;
43
44 let mut response = Response::new(url, ResourceFetchTiming::new(request.timing_type()));
45 response.headers.typed_insert(ContentType::html());
46 *response.body.lock() = ResponseBody::Done(output.into_bytes());
47
48 response
49}
50
51pub(crate) async fn build_html_directory_listing(
60 url: &Url,
61 path: PathBuf,
62 mut directory_contents: tokio::fs::ReadDir,
63) -> String {
64 let mut page_html = String::with_capacity(1024);
65 page_html.push_str("<!DOCTYPE html>");
66
67 let mut parent_url_string = String::new();
68 if path.parent().is_some() {
69 let mut parent_url = url.clone();
70 if let Ok(mut path_segments) = parent_url.path_segments_mut() {
71 path_segments.pop();
72 }
73 parent_url.as_str().clone_into(&mut parent_url_string);
74 }
75
76 page_html.push_str(&read_string(Resource::DirectoryListingHTML));
77
78 page_html.push_str("<script>\n");
79 write!(
80 page_html,
81 "setData({:?}, {:?}, [",
82 url.as_str(),
83 parent_url_string
84 )
85 .unwrap();
86
87 while let Ok(Some(directory_entry)) = directory_contents.next_entry().await {
88 let Ok(metadata) = directory_entry.metadata().await else {
89 continue;
90 };
91 write_directory_entry(directory_entry, metadata, url, &mut page_html);
92 }
93
94 page_html.push_str("]);");
95 page_html.push_str("</script>\n");
96
97 page_html
98}
99
100fn write_directory_entry(
101 entry: tokio::fs::DirEntry,
102 metadata: Metadata,
103 url: &Url,
104 output: &mut String,
105) {
106 let Ok(name) = entry.file_name().into_string() else {
107 return;
108 };
109
110 let mut file_url = url.clone();
111 {
112 let Ok(mut path_segments) = file_url.path_segments_mut() else {
113 return;
114 };
115 path_segments.push(&name);
116 }
117
118 let class = if metadata.is_dir() {
119 "directory"
120 } else if metadata.is_symlink() {
121 "symlink"
122 } else {
123 "file"
124 };
125
126 let file_url_string = file_url.as_str();
127 let file_size = metadata_to_file_size_string(&metadata);
128 let last_modified = metadata
129 .modified()
130 .map(DateTime::<Local>::from)
131 .map(|time| time.format("%F %r").to_string())
132 .unwrap_or_default();
133
134 let name = script_string_literal(&name);
139 write!(
140 output,
141 "[{class:?}, {name}, {file_url_string:?}, {file_size:?}, {last_modified:?}],"
142 )
143 .unwrap();
144}
145
146fn script_string_literal(value: &str) -> String {
151 format!("{value:?}").replace('<', "\\u003C")
152}
153
154pub fn metadata_to_file_size_string(metadata: &Metadata) -> String {
155 if !metadata.is_file() {
156 return String::new();
157 }
158
159 let mut float_size = metadata.len() as f64;
160 let mut prefix_power = 0;
161 while float_size > 1000.0 && prefix_power < 3 {
162 float_size /= 1000.0;
163 prefix_power += 1;
164 }
165
166 let prefix = match prefix_power {
167 0 => "B",
168 1 => "KB",
169 2 => "MB",
170 _ => "GB",
171 };
172
173 format!("{:.2} {prefix}", float_size)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::script_string_literal;
179
180 #[test]
181 fn script_string_literal_neutralises_script_close() {
182 let literal = script_string_literal("a</script><img src=x onerror=alert(1)>b");
183 assert!(!literal.contains('<'));
184 assert!(!literal.to_lowercase().contains("</script"));
185 }
186
187 #[test]
188 fn script_string_literal_keeps_debug_escaping() {
189 assert_eq!(script_string_literal("a\"b\\c"), r#""a\"b\\c""#);
192 assert_eq!(script_string_literal("plain"), r#""plain""#);
193 }
194}