Skip to main content

warp_runtime/
site_files.rs

1//! The files of a site (`warp build --site`, src/web/site.rs in warp) and how a request path finds one, shared by warp's
2//! servers and web view and by an app `warp build` made: the stub carrying a site packed (card build-links)
3use std::path::Path;
4
5pub const PAGE_FILE: &str = "index.html";
6/// The page of a program with routes for any other path (card single-page): a static host such as GitHub Pages serves
7/// 404.html for a path it has no file of, `warp dev` and `serve` do the same; the page's router shows that path's route
8pub const ROUTED_PAGE_FILE: &str = "404.html";
9const INDEX_PATH: &str = "/";
10/// The content types of a site's files, by their extension
11const CONTENT_TYPES: [(&str, &str); 3] =
12	[("html", "text/html; charset=utf-8"), ("js", "text/javascript; charset=utf-8"), ("wasm", "application/wasm")];
13const OTHER_CONTENT: &str = "application/octet-stream";
14/// What a packed site starts with, which machine code never does
15pub const PACKED_SITE: &[u8; 8] = b"WRPCsite";
16
17/// A file of a site: its name and content
18pub type SiteFile = (String, Vec<u8>);
19
20/// The file of a site a request path names: "/" is the page, "/app.wasm" the module, …, any other path the page of a
21/// program with routes (404.html)
22pub fn file_at<'a>(files: &'a [SiteFile], path: &str) -> Option<&'a SiteFile> {
23	let name = if path == INDEX_PATH { PAGE_FILE } else { path.trim_start_matches('/') };
24	let named = |wanted: &str| files.iter().find(|(file, _)| file == wanted);
25	named(name).or_else(|| named(ROUTED_PAGE_FILE))
26}
27
28/// The content type of a site's file, by its extension
29pub fn content_type(name: &str) -> &'static str {
30	let extension = Path::new(name).extension().and_then(|extension| extension.to_str()).unwrap_or_default();
31	CONTENT_TYPES.iter().find(|(known, _)| *known == extension).map_or(OTHER_CONTENT, |(_, content_type)| content_type)
32}
33
34/// A path's file and its content type, as the web view answers it
35pub fn served(files: &[SiteFile], path: &str) -> Option<(Vec<u8>, &'static str)> {
36	file_at(files, path).map(|(name, bytes)| (bytes.clone(), content_type(name)))
37}
38
39/// The site as one run of bytes: PACKED_SITE, then each file's name and content, each after its length (u64 LE)
40pub fn packed(files: &[SiteFile]) -> Vec<u8> {
41	let mut bytes = PACKED_SITE.to_vec();
42	for part in files.iter().flat_map(|(name, content)| [name.as_bytes(), content.as_slice()]) {
43		bytes.extend_from_slice(&(part.len() as u64).to_le_bytes());
44		bytes.extend_from_slice(part);
45	}
46	bytes
47}
48
49/// The site `packed` made, None for any other bytes
50pub fn unpacked(bytes: &[u8]) -> Option<Vec<SiteFile>> {
51	let mut rest = bytes.strip_prefix(PACKED_SITE.as_slice())?;
52	let mut next = || -> Option<Vec<u8>> {
53		let (length, after) = rest.split_first_chunk::<8>()?;
54		let (part, after) = after.split_at_checked(u64::from_le_bytes(*length) as usize)?;
55		rest = after;
56		Some(part.to_vec())
57	};
58	let mut files = vec![];
59	while let Some(name) = next() {
60		files.push((String::from_utf8(name).ok()?, next()?));
61	}
62	Some(files)
63}