use crate::model::{File, MAX_FILE_BYTES, Manifest, validate_file_path, validate_module_name};
use crate::storage;
use crate::{COORDINATOR_DIRECTORY, Error, Result};
use fs2::FileExt;
use semver::Version;
use serde::Deserialize;
use std::fs::{self, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use uuid::Uuid;
const COORDINATOR_FILE: &str = "kcode-web-libs-check-v1.html";
const CHECK_TIMEOUT: Duration = Duration::from_secs(30);
const CONNECTION_TIMEOUT: Duration = Duration::from_secs(2);
const MAX_HEADER_BYTES: usize = 32 * 1024;
const MAX_RESULT_BYTES: usize = 32 * 1024;
const MAX_DIAGNOSTIC_BYTES: u64 = 16 * 1024;
const COORDINATOR_HTML: &str = r#"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>kcode-web-libs check</title>
</head>
<body>
<script type="module">
const stringifyError = (error) => {
const value = error && error.stack ? error.stack : String(error);
return String(value).slice(0, 16384);
};
const report = async (result, path) => {
const response = await fetch(path, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(result),
cache: "no-store"
});
if (!response.ok) {
throw new Error(`check result was rejected with HTTP ${response.status}`);
}
};
try {
const configResponse = await fetch("/__kcode_config", { cache: "no-store" });
if (!configResponse.ok) {
throw new Error(`check config failed with HTTP ${configResponse.status}`);
}
const config = await configResponse.json();
await import(config.entry);
const tests = await import(config.tests);
if (typeof tests.runTests !== "function") {
throw new Error("the declared test module must export runTests()");
}
await tests.runTests();
await report({ ok: true }, config.result);
} catch (error) {
try {
const configResponse = await fetch("/__kcode_config", { cache: "no-store" });
const config = await configResponse.json();
await report({ ok: false, error: stringifyError(error) }, config.result);
} catch (reportError) {
document.body.textContent =
`Unable to report check failure: ${stringifyError(error)}; ` +
stringifyError(reportError);
}
}
</script>
</body>
</html>
"#;
struct ServerState {
candidate_name: String,
candidate_version: String,
candidate_root: PathBuf,
published_root: PathBuf,
coordinator_path: PathBuf,
entry_url: String,
tests_url: String,
result_path: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct BrowserReport {
ok: bool,
error: Option<String>,
}
#[derive(Debug)]
struct Request {
method: String,
target: String,
body: Vec<u8>,
}
pub(crate) fn check(root: &Path, name: &str, files: &[File], manifest: &Manifest) -> Result<()> {
validate_module_name(name)?;
if manifest.name != name {
return Err(Error::invalid_source(
"manifest name does not match the managed library",
));
}
let parsed_version = Version::parse(&manifest.version).map_err(|error| {
Error::invalid_source(format!(
"manifest version `{}` is not valid SemVer: {error}",
manifest.version
))
})?;
if parsed_version.to_string() != manifest.version {
return Err(Error::invalid_source(format!(
"manifest version must use canonical SemVer spelling `{parsed_version}`"
)));
}
let coordinator_path = ensure_coordinator(&root.join(COORDINATOR_DIRECTORY))?;
let candidate = tempfile::tempdir()
.map_err(|error| Error::io("create disposable browser source directory", error))?;
materialize_candidate(candidate.path(), files)?;
let candidate_root = candidate.path().to_path_buf();
let route_root = format!("/module/{name}/v{}", manifest.version);
let result_path = format!("/__kcode_result/{}", Uuid::new_v4());
let state = ServerState {
candidate_name: name.to_owned(),
candidate_version: manifest.version.clone(),
candidate_root,
published_root: root.to_path_buf(),
coordinator_path,
entry_url: format!("{route_root}/{}", encode_url_path(&manifest.entry)),
tests_url: format!("{route_root}/{}", encode_url_path(&manifest.tests)),
result_path,
};
let listener = TcpListener::bind(("127.0.0.1", 0))
.map_err(|error| Error::io("bind temporary check server", error))?;
listener
.set_nonblocking(true)
.map_err(|error| Error::io("configure temporary check server", error))?;
let address = listener
.local_addr()
.map_err(|error| Error::io("read temporary check server address", error))?;
let coordinator_url = format!("http://{address}/__kcode_check");
let profile = tempfile::tempdir()
.map_err(|error| Error::io("create temporary Chromium profile", error))?;
let stderr_writer = tempfile::tempfile()
.map_err(|error| Error::io("create Chromium diagnostic file", error))?;
let mut stderr_reader = stderr_writer
.try_clone()
.map_err(|error| Error::io("clone Chromium diagnostic file", error))?;
let mut child = Command::new("chromium")
.arg("--headless")
.arg("--disable-gpu")
.arg("--disable-background-networking")
.arg("--disable-component-update")
.arg("--disable-default-apps")
.arg("--disable-extensions")
.arg("--disable-sync")
.arg("--metrics-recording-only")
.arg("--mute-audio")
.arg("--no-default-browser-check")
.arg("--no-first-run")
.arg(format!("--user-data-dir={}", profile.path().display()))
.arg(&coordinator_url)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::from(stderr_writer))
.spawn()
.map_err(|error| {
Error::new(format!(
"check.browser_start: could not start `chromium` from PATH: {error}"
))
})?;
let outcome = serve_until_report(&listener, &state, &mut child);
let cleanup = terminate_child(&mut child);
let diagnostic = read_diagnostic(&mut stderr_reader);
match (outcome, cleanup) {
(Ok(()), Ok(())) => Ok(()),
(Ok(()), Err(cleanup_error)) => Err(with_diagnostic(cleanup_error, &diagnostic)),
(Err(error), Ok(())) => Err(with_diagnostic(error, &diagnostic)),
(Err(error), Err(cleanup_error)) => Err(with_diagnostic(
Error::new(format!(
"{error}\nChromium cleanup also failed: {cleanup_error}"
)),
&diagnostic,
)),
}
}
fn materialize_candidate(root: &Path, files: &[File]) -> Result<()> {
for file in files {
let destination = root.join(&file.path);
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)
.map_err(|error| Error::io("create disposable browser source directory", error))?;
}
let mut output = OpenOptions::new()
.create_new(true)
.write(true)
.open(&destination)
.map_err(|error| Error::io("create disposable browser source file", error))?;
output
.write_all(file.contents.as_bytes())
.map_err(|error| Error::io("write disposable browser source file", error))?;
}
Ok(())
}
fn ensure_coordinator(root: &Path) -> Result<PathBuf> {
storage::create_dir_all_durable(root, "create coordinator directory")?;
let root_metadata = fs::symlink_metadata(root)
.map_err(|error| Error::io("inspect coordinator directory", error))?;
if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
return Err(Error::new(
"check.infrastructure: coordinator root is not a regular directory",
));
}
let lock_path = root.join(".kcode-web-libs.lock");
match fs::symlink_metadata(&lock_path) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
return Err(Error::new(
"check.infrastructure: coordinator lock is not a regular file",
));
}
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(Error::io("inspect coordinator lock", error)),
}
let lock = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&lock_path)
.map_err(|error| Error::io("open coordinator lock", error))?;
if !lock
.metadata()
.map_err(|error| Error::io("inspect opened coordinator lock", error))?
.is_file()
{
return Err(Error::new(
"check.infrastructure: opened coordinator lock is not a regular file",
));
}
FileExt::lock_exclusive(&lock)
.map_err(|error| Error::io("lock coordinator directory", error))?;
let path = root.join(COORDINATOR_FILE);
match OpenOptions::new().create_new(true).write(true).open(&path) {
Ok(mut file) => {
if let Err(error) = (|| {
file.write_all(COORDINATOR_HTML.as_bytes())
.map_err(|error| Error::io("write coordinator page", error))?;
file.sync_all()
.map_err(|error| Error::io("sync coordinator page", error))
})() {
drop(file);
if fs::remove_file(&path).is_ok() {
let _ = storage::sync_directory(root);
}
return Err(error);
}
storage::sync_directory(root)?;
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
let metadata = fs::symlink_metadata(&path)
.map_err(|error| Error::io("inspect coordinator page", error))?;
if !metadata.file_type().is_file() || metadata.file_type().is_symlink() {
return Err(Error::new(
"check.infrastructure: coordinator page is not a regular file",
));
}
let existing = fs::read(&path)
.map_err(|error| Error::io("read existing coordinator page", error))?;
if existing != COORDINATOR_HTML.as_bytes() {
return Err(Error::new(format!(
"check.infrastructure: `{COORDINATOR_FILE}` does not match the expected v1 coordinator"
)));
}
}
Err(error) => return Err(Error::io("create coordinator page", error)),
}
drop(lock);
Ok(path)
}
fn serve_until_report(
listener: &TcpListener,
state: &ServerState,
child: &mut Child,
) -> Result<()> {
let deadline = Instant::now() + CHECK_TIMEOUT;
loop {
if Instant::now() >= deadline {
return Err(Error::new(format!(
"check.timeout: no test result was received within {} seconds",
CHECK_TIMEOUT.as_secs()
)));
}
if let Some(status) = child
.try_wait()
.map_err(|error| Error::io("poll Chromium", error))?
{
return Err(Error::new(format!(
"check.browser_exit: Chromium exited before reporting test results with status {status}"
)));
}
match listener.accept() {
Ok((stream, _)) => {
if let Some(report) = handle_connection(stream, state)? {
if report.ok {
return Ok(());
}
let error = report
.error
.unwrap_or_else(|| "module tests reported failure".to_owned());
return Err(Error::new(format!("check.test: {error}")));
}
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
Err(error) => return Err(Error::io("accept temporary check connection", error)),
}
}
}
fn handle_connection(mut stream: TcpStream, state: &ServerState) -> Result<Option<BrowserReport>> {
stream
.set_read_timeout(Some(CONNECTION_TIMEOUT))
.map_err(|error| Error::io("configure check request timeout", error))?;
stream
.set_write_timeout(Some(CONNECTION_TIMEOUT))
.map_err(|error| Error::io("configure check response timeout", error))?;
let Some(request) = read_request(&mut stream)? else {
return Ok(None);
};
let path = request.target.split('?').next().unwrap_or("");
if request.method == "GET" && path == "/__kcode_check" {
let body = fs::read(&state.coordinator_path)
.map_err(|error| Error::io("read coordinator page", error))?;
send_response(&mut stream, "200 OK", "text/html; charset=utf-8", &body)?;
return Ok(None);
}
if request.method == "GET" && path == "/__kcode_config" {
let body = serde_json::to_vec(&serde_json::json!({
"entry": state.entry_url,
"tests": state.tests_url,
"result": state.result_path,
}))
.map_err(|error| Error::new(format!("check.protocol: encode config: {error}")))?;
send_response(&mut stream, "200 OK", "application/json", &body)?;
return Ok(None);
}
if request.method == "POST" && path == state.result_path {
if request.body.len() > MAX_RESULT_BYTES {
send_response(
&mut stream,
"413 Payload Too Large",
"text/plain; charset=utf-8",
b"result too large",
)?;
return Err(Error::new(
"check.protocol: browser result exceeded the size limit",
));
}
let report: BrowserReport = serde_json::from_slice(&request.body)
.map_err(|error| Error::new(format!("check.protocol: invalid result: {error}")))?;
send_response(&mut stream, "204 No Content", "text/plain", b"")?;
return Ok(Some(report));
}
if request.method == "GET" {
if let Some((body, content_type)) = read_module_file(path, state)? {
send_response(&mut stream, "200 OK", content_type, &body)?;
} else {
send_response(
&mut stream,
"404 Not Found",
"text/plain; charset=utf-8",
b"not found",
)?;
}
return Ok(None);
}
send_response(
&mut stream,
"405 Method Not Allowed",
"text/plain; charset=utf-8",
b"method not allowed",
)?;
Ok(None)
}
fn read_request(stream: &mut TcpStream) -> Result<Option<Request>> {
let mut bytes = Vec::new();
let mut buffer = [0u8; 4096];
let header_end;
loop {
let count = match stream.read(&mut buffer) {
Ok(count) => count,
Err(error)
if bytes.is_empty()
&& matches!(
error.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) =>
{
return Ok(None);
}
Err(error) => return Err(Error::io("read check request", error)),
};
if count == 0 {
if bytes.is_empty() {
return Ok(None);
}
return Err(Error::new(
"check.protocol: connection closed before headers completed",
));
}
bytes.extend_from_slice(&buffer[..count]);
if bytes.len() > MAX_HEADER_BYTES {
return Err(Error::new(
"check.protocol: headers exceeded the size limit",
));
}
if let Some(position) = find_subsequence(&bytes, b"\r\n\r\n") {
header_end = position + 4;
break;
}
}
let header_text = std::str::from_utf8(&bytes[..header_end])
.map_err(|_| Error::new("check.protocol: headers were not valid UTF-8"))?;
let mut lines = header_text.split("\r\n");
let request_line = lines
.next()
.ok_or_else(|| Error::new("check.protocol: missing request line"))?;
let mut request_parts = request_line.split_whitespace();
let method = request_parts
.next()
.ok_or_else(|| Error::new("check.protocol: missing method"))?;
let target = request_parts
.next()
.ok_or_else(|| Error::new("check.protocol: missing target"))?;
let version = request_parts
.next()
.ok_or_else(|| Error::new("check.protocol: missing HTTP version"))?;
if request_parts.next().is_some()
|| !matches!(version, "HTTP/1.0" | "HTTP/1.1")
|| !target.starts_with('/')
{
return Err(Error::new("check.protocol: malformed request line"));
}
let method = method.to_owned();
let target = target.to_owned();
let mut content_length = 0usize;
for line in lines {
if line.is_empty() {
continue;
}
let (name, value) = line
.split_once(':')
.ok_or_else(|| Error::new("check.protocol: malformed header"))?;
if name.eq_ignore_ascii_case("content-length") {
content_length = value
.trim()
.parse::<usize>()
.map_err(|_| Error::new("check.protocol: invalid content length"))?;
if content_length > MAX_RESULT_BYTES {
return Err(Error::new("check.protocol: body exceeded the size limit"));
}
}
}
let required = header_end
.checked_add(content_length)
.ok_or_else(|| Error::new("check.protocol: request size overflow"))?;
while bytes.len() < required {
let count = stream
.read(&mut buffer)
.map_err(|error| Error::io("read check request body", error))?;
if count == 0 {
return Err(Error::new(
"check.protocol: connection closed before body completed",
));
}
bytes.extend_from_slice(&buffer[..count]);
if bytes.len() > required {
bytes.truncate(required);
break;
}
}
Ok(Some(Request {
method,
target,
body: bytes[header_end..required].to_vec(),
}))
}
fn read_module_file(
request_path: &str,
state: &ServerState,
) -> Result<Option<(Vec<u8>, &'static str)>> {
let Some(route) = parse_module_route(request_path)? else {
return Ok(None);
};
let root = if route.name == state.candidate_name && route.version == state.candidate_version {
state.candidate_root.clone()
} else {
state
.published_root
.join("module")
.join(&route.name)
.join(format!("v{}", route.version))
};
let Some(bytes) = read_regular_descendant(&root, &route.file)? else {
return Ok(None);
};
Ok(Some((bytes, content_type(&route.file))))
}
struct ModuleRoute {
name: String,
version: String,
file: String,
}
fn parse_module_route(path: &str) -> Result<Option<ModuleRoute>> {
let Some(remainder) = path.strip_prefix("/module/") else {
return Ok(None);
};
let mut parts = remainder.splitn(3, '/');
let Some(name) = parts.next() else {
return Ok(None);
};
let Some(version_segment) = parts.next() else {
return Ok(None);
};
let Some(encoded_file) = parts.next() else {
return Ok(None);
};
if name.is_empty() || version_segment.is_empty() || encoded_file.is_empty() {
return Ok(None);
}
validate_module_name(name)?;
let version_text = version_segment
.strip_prefix('v')
.ok_or_else(|| Error::new("check.protocol: module version must begin with `v`"))?;
let version = Version::parse(version_text).map_err(|error| {
Error::new(format!(
"check.protocol: invalid module version `{version_text}`: {error}"
))
})?;
if version.to_string() != version_text {
return Err(Error::new(format!(
"check.protocol: module version must use canonical SemVer spelling `{version}`"
)));
}
let file = decode_url_path(encoded_file)?;
validate_file_path(&file)?;
Ok(Some(ModuleRoute {
name: name.to_owned(),
version: version_text.to_owned(),
file,
}))
}
fn encode_url_path(path: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut encoded = String::with_capacity(path.len());
for byte in path.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'/') {
encoded.push(char::from(byte));
} else {
encoded.push('%');
encoded.push(char::from(HEX[(byte >> 4) as usize]));
encoded.push(char::from(HEX[(byte & 0x0f) as usize]));
}
}
encoded
}
fn decode_url_path(path: &str) -> Result<String> {
let bytes = path.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] != b'%' {
decoded.push(bytes[index]);
index += 1;
continue;
}
if index + 2 >= bytes.len() {
return Err(Error::new(
"check.protocol: incomplete percent-encoded path byte",
));
}
let high = hex_value(bytes[index + 1])
.ok_or_else(|| Error::new("check.protocol: invalid percent-encoded path byte"))?;
let low = hex_value(bytes[index + 2])
.ok_or_else(|| Error::new("check.protocol: invalid percent-encoded path byte"))?;
decoded.push((high << 4) | low);
index += 3;
}
String::from_utf8(decoded).map_err(|_| Error::new("check.protocol: path is not valid UTF-8"))
}
fn hex_value(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
fn read_regular_descendant(root: &Path, relative: &str) -> Result<Option<Vec<u8>>> {
let root_metadata = match fs::symlink_metadata(root) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(Error::io("inspect module root", error)),
};
if !root_metadata.is_dir() || root_metadata.file_type().is_symlink() {
return Err(Error::new(
"check.source: module root is not a regular directory",
));
}
let components = relative.split('/').collect::<Vec<_>>();
let mut current = root.to_path_buf();
for (index, component) in components.iter().enumerate() {
current.push(component);
let metadata = match fs::symlink_metadata(¤t) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(Error::io("inspect module file", error)),
};
if metadata.file_type().is_symlink() {
return Err(Error::new(
"check.source: module paths may not contain symlinks",
));
}
if index + 1 == components.len() {
if !metadata.is_file() {
return Ok(None);
}
let length = usize::try_from(metadata.len())
.map_err(|_| Error::new("check.source: module file length overflow"))?;
if length > MAX_FILE_BYTES {
return Err(Error::new(format!(
"check.source: served file exceeds the {MAX_FILE_BYTES} byte limit"
)));
}
} else if !metadata.is_dir() {
return Ok(None);
}
}
fs::read(¤t)
.map(Some)
.map_err(|error| Error::io("read module file", error))
}
fn content_type(path: &str) -> &'static str {
let extension = path.rsplit_once('.').map(|(_, extension)| extension);
match extension {
Some("js" | "mjs") => "text/javascript; charset=utf-8",
Some("css") => "text/css; charset=utf-8",
Some("html" | "htm") => "text/html; charset=utf-8",
Some("json") => "application/json",
Some("svg") => "image/svg+xml",
Some("txt" | "md") => "text/plain; charset=utf-8",
Some("xml") => "application/xml",
_ => "application/octet-stream",
}
}
fn send_response(
stream: &mut TcpStream,
status: &str,
content_type: &str,
body: &[u8],
) -> Result<()> {
let header = format!(
"HTTP/1.1 {status}\r\n\
Content-Length: {}\r\n\
Content-Type: {content_type}\r\n\
Cache-Control: no-store\r\n\
X-Content-Type-Options: nosniff\r\n\
Connection: close\r\n\
\r\n",
body.len()
);
stream
.write_all(header.as_bytes())
.and_then(|_| stream.write_all(body))
.and_then(|_| stream.flush())
.map_err(|error| Error::io("write check response", error))
}
fn terminate_child(child: &mut Child) -> Result<()> {
if child
.try_wait()
.map_err(|error| Error::io("poll Chromium during cleanup", error))?
.is_some()
{
return Ok(());
}
child
.kill()
.map_err(|error| Error::io("terminate Chromium", error))?;
child
.wait()
.map_err(|error| Error::io("wait for Chromium", error))?;
Ok(())
}
fn read_diagnostic(file: &mut fs::File) -> String {
if file.seek(SeekFrom::Start(0)).is_err() {
return String::new();
}
let mut bytes = Vec::new();
if file
.take(MAX_DIAGNOSTIC_BYTES + 1)
.read_to_end(&mut bytes)
.is_err()
{
return String::new();
}
let truncated = bytes.len() as u64 > MAX_DIAGNOSTIC_BYTES;
if truncated {
bytes.truncate(MAX_DIAGNOSTIC_BYTES as usize);
}
let mut text = String::from_utf8_lossy(&bytes).trim().to_owned();
if truncated {
text.push_str("\n[Chromium diagnostic truncated]");
}
text
}
fn with_diagnostic(error: Error, diagnostic: &str) -> Error {
if diagnostic.is_empty() {
error
} else {
Error::new(format!("{error}\nChromium diagnostic:\n{diagnostic}"))
}
}
fn find_subsequence(bytes: &[u8], needle: &[u8]) -> Option<usize> {
bytes
.windows(needle.len())
.position(|window| window == needle)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn creates_and_reuses_the_exact_coordinator() {
let root = TempDir::new().unwrap();
let first = ensure_coordinator(root.path()).unwrap();
let second = ensure_coordinator(root.path()).unwrap();
assert_eq!(first, second);
assert_eq!(fs::read_to_string(first).unwrap(), COORDINATOR_HTML);
}
#[test]
fn parses_only_exact_versioned_module_routes() {
let route = parse_module_route("/module/chat-page/v1.2.3/components/button.js")
.unwrap()
.unwrap();
assert_eq!(route.name, "chat-page");
assert_eq!(route.version, "1.2.3");
assert_eq!(route.file, "components/button.js");
assert!(
parse_module_route("/other/chat-page/v1.2.3/index.js")
.unwrap()
.is_none()
);
assert!(parse_module_route("/module/chat-page/1.2.3/index.js").is_err());
assert!(parse_module_route("/module/chat-page/v01.2.3/index.js").is_err());
assert!(parse_module_route("/module/chat-page/v1.2.3/../index.js").is_err());
let encoded = parse_module_route("/module/Example_lib/v1.2.3/src/entr%C3%A9e%20module.js")
.unwrap()
.unwrap();
assert_eq!(encoded.name, "Example_lib");
assert_eq!(encoded.file, "src/entrée module.js");
assert_eq!(
encode_url_path(&encoded.file),
"src/entr%C3%A9e%20module.js"
);
}
#[test]
fn reads_regular_descendants_without_symlink_escape() {
let root = TempDir::new().unwrap();
fs::create_dir_all(root.path().join("nested")).unwrap();
fs::write(
root.path().join("nested/value.js"),
"export const value = 7;",
)
.unwrap();
assert_eq!(
read_regular_descendant(root.path(), "nested/value.js").unwrap(),
Some(b"export const value = 7;".to_vec())
);
assert_eq!(
read_regular_descendant(root.path(), "missing.js").unwrap(),
None
);
}
#[test]
fn assigns_browser_module_content_types() {
assert_eq!(content_type("index.js"), "text/javascript; charset=utf-8");
assert_eq!(content_type("style.css"), "text/css; charset=utf-8");
assert_eq!(content_type("data.json"), "application/json");
assert_eq!(content_type("unknown.bin"), "application/octet-stream");
}
#[test]
fn reads_a_request_with_a_body() {
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let address = listener.local_addr().unwrap();
let client = thread::spawn(move || {
let mut stream = TcpStream::connect(address).unwrap();
stream
.write_all(b"POST /result HTTP/1.1\r\nContent-Length: 11\r\n\r\n{\"ok\":true}")
.unwrap();
});
let (mut stream, _) = listener.accept().unwrap();
let request = read_request(&mut stream).unwrap().unwrap();
client.join().unwrap();
assert_eq!(request.method, "POST");
assert_eq!(request.target, "/result");
assert_eq!(request.body, br#"{"ok":true}"#);
}
#[test]
fn ignores_an_unused_preconnected_socket() {
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let address = listener.local_addr().unwrap();
let client = TcpStream::connect(address).unwrap();
let (mut stream, _) = listener.accept().unwrap();
drop(client);
assert!(read_request(&mut stream).unwrap().is_none());
}
#[test]
fn rejects_a_truncated_nonempty_request() {
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let address = listener.local_addr().unwrap();
let mut client = TcpStream::connect(address).unwrap();
let (mut stream, _) = listener.accept().unwrap();
client.write_all(b"GET /incomplete HTTP/1.1\r\n").unwrap();
drop(client);
let error = read_request(&mut stream).unwrap_err();
assert_eq!(
error.to_string(),
"check.protocol: connection closed before headers completed"
);
}
}