//! Node-prefixed W0 HTTP routing over the serving registry. //! //! The listener remains in `docshost`; docs bytes and directory containment use //! its existing sanitizer and serving implementation. Registry state is loaded //! from the explicitly supplied home on every index/file request. use std::fmt::Write as _; use std::path::{Component, Path}; use http_body_util::Full; use hyper::body::Bytes; use hyper::{Response, StatusCode}; use serde::Serialize; use spt_store::serving::{alias_url, encode_url_segment, entry_url, ServedEntry, ServedKind, ServingRegistry}; use crate::docshost::{content_type_for, sanitize_request_path, serve_path}; fn response( status: StatusCode, content_type: &'static str, body: Vec, head_only: bool, ) -> Response> { Response::builder() .status(status) .header("content-type", content_type) .header("content-length", body.len()) .body(Full::new(if head_only { Bytes::new() } else { Bytes::from(body) })) .expect("static response builds") } fn text(status: StatusCode, body: String, head_only: bool) -> Response> { response(status, "text/plain; charset=utf-8", body.into_bytes(), head_only) } fn redirect(mut location: String, query: Option<&str>) -> Response> { if let Some(query) = query { location.push('?'); location.push_str(query); } Response::builder() .status(StatusCode::FOUND) .header("location", location) .body(Full::new(Bytes::new())) .expect("encoded redirect builds") } fn redirect_node(node: &str, query: Option<&str>) -> Response> { redirect(format!("/{}/", encode_url_segment(&node.to_ascii_lowercase())), query) } fn redirect_directory(uri_path: &str, query: Option<&str>) -> Response> { // A path-only Location, even if the request supplied repeated leading slashes. redirect(format!("/{}/", uri_path.trim_start_matches('/')), query) } /// Decode a routing segment with the docs sanitizer, without allowing an /// encoded separator to change the node/facet/name boundary. Docs subpaths /// themselves keep the existing sanitizer's full path semantics. fn route_segment(raw: &str) -> Option { if raw.is_empty() || raw.as_bytes().windows(3).any(|s| s[0] == b'%' && s[1] == b'2' && matches!(s[2], b'f' | b'F')) { return None; } let path = sanitize_request_path(raw)?; let mut components = path.components(); let Component::Normal(segment) = components.next()? else { return None; }; let segment = segment.to_str()?; if components.next().is_some() { return None; } Some(segment.to_owned()) } /// Match node labels, not endpoint IDs. Membership is loaded for this home on /// every request so joining/leaving a subnet changes only the compatibility /// alias's shadow, never the canonical local docs URL. fn is_known_subnet_node(home: &Path, label: &str) -> bool { let identity = home.join("identity"); let subnets = spt_store::subnet::SubnetStore::load_from(&identity.join("subnet.json")); let roster = spt_store::roster::RosterStore::load_from(&identity.join("roster.json")); subnets.subnets.iter().any(|subnet| { let path = crate::registryhost::RegistryHost::snapshot_path(&identity.join("registry"), &subnet.name); let registry: spt_net::net::registry::SubnetRegistry = std::fs::read(path) .ok() .and_then(|bytes| serde_json::from_slice(&bytes).ok()) .unwrap_or_default(); roster.members_in(&subnet.name).any(|member| { let advertised = registry.node_labels() .find_map(|(node, label)| (node == member.pubkey_hex).then_some(label)) .unwrap_or(&member.label); advertised.eq_ignore_ascii_case(label) }) }) } fn escape_html(value: &str) -> String { let mut escaped = String::with_capacity(value.len()); for c in value.chars() { match c { '&' => escaped.push_str("&"), '<' => escaped.push_str("<"), '>' => escaped.push_str(">"), '"' => escaped.push_str("""), '\'' => escaped.push_str("'"), _ => escaped.push(c), } } escaped } #[derive(Serialize)] struct IndexEntry<'a> { #[serde(flatten)] entry: &'a ServedEntry, url: String, #[serde(skip_serializing_if = "Option::is_none")] alias_url: Option, } #[derive(Serialize)] struct Index<'a> { node: &'a str, entries: Vec>, } // [impl->REQ-WEB-SERVING-REGISTRY] fn registry_at(home: &Path) -> Result { let path = spt_store::perch::serving_registry_file_in(home); ServingRegistry::load_at(&path) .map_err(|error| format!("SERVING_REGISTRY_LOAD_FAIL: {}: {error}\n", path.display())) } // [impl->REQ-WEB-URL-NODE-PREFIX] fn index( registry: &ServingRegistry, node: &str, port: u16, query: Option<&str>, head_only: bool, ) -> Response> { let node = node.to_ascii_lowercase(); let entries: Vec<_> = registry.entries().map(|entry| IndexEntry { entry, url: entry_url(&node, port, entry), alias_url: alias_url(&node, port, entry), }).collect(); if query.is_some_and(|q| q.split('&').any(|part| part.split('=').next() == Some("json"))) { return match serde_json::to_vec(&Index { node: &node, entries }) { Ok(bytes) => response(StatusCode::OK, "application/json", bytes, head_only), Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, format!("SERVING_INDEX_FAIL: {error}\n"), head_only), }; } let escaped_node = escape_html(&node); let mut html = format!( "Served resources — {escaped_node}

Served resources — {escaped_node}

" ); for row in entries { let url = escape_html(&row.url); write!(html, ""); } html.push_str("
Source pathURL
{}{url}", escape_html(&row.entry.path.to_string_lossy())) .expect("writing to a String cannot fail"); if let Some(alias) = row.alias_url { let alias = escape_html(&alias); write!(html, " (alias: {alias})") .expect("writing to a String cannot fail"); } html.push_str("
\n"); response(StatusCode::OK, "text/html; charset=utf-8", html.into_bytes(), head_only) } // [impl->REQ-WEB-SERVING-REGISTRY] fn reference( home: &Path, registry: &ServingRegistry, remainder: &str, uri_path: &str, query: Option<&str>, head_only: bool, ) -> Response> { let (raw_name, subpath) = remainder.split_once('/').unwrap_or((remainder, "")); let Some(name) = route_segment(raw_name) else { return text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one served-name segment\n".to_owned(), head_only); }; // HTTP names are names, not management IDs. An ID accepted by `serve rm` // must not turn into an unlisted second URL (or shadow another entry). let Some(entry) = registry.entries().find(|entry| entry.served_name == name) else { return text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\n"), head_only); }; serve_entry(home, entry, subpath, uri_path, query, head_only) } // [impl->REQ-WEB-SERVING-REGISTRY] fn serve_entry( home: &Path, entry: &ServedEntry, subpath: &str, uri_path: &str, query: Option<&str>, head_only: bool, ) -> Response> { let name = &entry.served_name; if let Some(adapter) = &entry.adapter { let valid_root = spt_store::perch::validate_adapter_web_dir_in(home, adapter); if !matches!((valid_root, entry.path.canonicalize()), (Ok(expected), Ok(actual)) if expected == actual) { return text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\n"), head_only); } } match entry.kind { ServedKind::Dir if entry.path.is_dir() => { // The root index must be addressed as a directory so relative links // stay below the served root. Docs compatibility paths are untouched. if subpath.is_empty() && !uri_path.ends_with('/') { return redirect_directory(uri_path, query); } serve_path(&entry.path, subpath, head_only) } ServedKind::File if subpath.is_empty() && entry.path.is_file() => { match std::fs::read(&entry.path) { Ok(bytes) => response(StatusCode::OK, content_type_for(&entry.path), bytes, head_only), Err(error) if error.kind() == std::io::ErrorKind::NotFound => { text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\n"), head_only) } Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, format!("READ_FAIL: {error}\n"), head_only), } } // Attachment byte serving belongs to W2, even if an entry of that // kind already exists in the forward-compatible registry format. _ => text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\n"), head_only), } } // [impl->REQ-WEB-SERVING-REGISTRY] fn adapter( home: &Path, registry: &ServingRegistry, remainder: &str, uri_path: &str, query: Option<&str>, head_only: bool, ) -> Response> { let (raw_adapter, subpath) = remainder.split_once('/').unwrap_or((remainder, "")); let Some(name) = route_segment(raw_adapter) else { return text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one adapter segment\n".to_owned(), head_only); }; match registry.entries().find(|entry| entry.adapter.as_deref() == Some(name.as_str())) { Some(entry) => serve_entry(home, entry, subpath, uri_path, query, head_only), None => text(StatusCode::NOT_FOUND, format!("NOT_FOUND: facet a adapter {name}\n"), head_only), } } /// Resolve one GET/HEAD path. The production listener owns method selection; /// this module owns only routing and never reads the process's SPT_HOME. // [impl->REQ-WEB-URL-NODE-PREFIX] pub fn handle_path( home: &Path, docs_root: &Path, local_node: &str, port: u16, uri_path: &str, query: Option<&str>, head_only: bool, ) -> Response> { if uri_path == "/" { return redirect_node(local_node, query); } let Some(relative) = sanitize_request_path(uri_path) else { let raw_first = uri_path.trim_start_matches('/').split('/').next().unwrap_or(""); if route_segment(raw_first).is_some_and(|first| { first.eq_ignore_ascii_case(local_node) && !spt_store::hostlabel::is_reserved_web_facet(&first) }) { return text(StatusCode::BAD_REQUEST, "BAD_PATH: rejected by the docs-root sanitizer\n".to_owned(), head_only); } // Preserve the docs-less compatibility response as well as the // installed bundle's sanitizer status; no bytes bypass containment. return serve_path(docs_root, uri_path, head_only); }; // ADR-0056 Amendment 1: local node, exact docs root-file leaf, known peer, // then the unchanged docs compatibility surface (including its docs 404). let first = relative.components().next().and_then(|component| match component { Component::Normal(first) => first.to_str(), _ => None, }); let Some(first) = first else { return serve_path(docs_root, uri_path, head_only); }; if spt_store::hostlabel::is_reserved_web_facet(first) { return serve_path(docs_root, uri_path, head_only); } let path = uri_path.trim_start_matches('/'); let bare_node = !path.contains('/') && route_segment(path).is_some(); if !first.eq_ignore_ascii_case(local_node) { // Rule 2.5 protects root files by URL shape, not hostname policy. // A trailing slash or child segment always keeps the node grammar. if bare_node && docs_root.join(first).is_file() { return serve_path(docs_root, uri_path, head_only); } if is_known_subnet_node(home, first) { if bare_node { return redirect_node(first, query); } return text(StatusCode::BAD_GATEWAY, format!("NODE_UNAVAILABLE: {first}: cross-node serving is not available yet\n"), head_only); } return serve_path(docs_root, uri_path, head_only); } if bare_node { return redirect_node(local_node, query); } let (raw_node, remainder) = path.split_once('/').unwrap_or((path, "")); let Some(_) = route_segment(raw_node) else { return text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one node segment\n".to_owned(), head_only); }; if remainder.is_empty() { return match registry_at(home) { Ok(registry) => index(®istry, local_node, port, query, head_only), Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only), }; } let (raw_facet, remainder) = remainder.split_once('/').unwrap_or((remainder, "")); let Some(facet) = route_segment(raw_facet) else { return text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one facet segment\n".to_owned(), head_only); }; match facet.as_str() { "docs" => { if remainder.is_empty() && !uri_path.ends_with('/') { return redirect_directory(uri_path, query); } serve_path(docs_root, remainder, head_only) } "f" if !remainder.is_empty() => match registry_at(home) { Ok(registry) => reference(home, ®istry, remainder, uri_path, query, head_only), Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only), }, "a" if !remainder.is_empty() => match registry_at(home) { Ok(registry) => adapter(home, ®istry, remainder, uri_path, query, head_only), Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only), }, "m" | "bin" | "install" => text(StatusCode::NOT_FOUND, format!("FACET_UNAVAILABLE: {facet}\n"), head_only), "a" | "f" => text(StatusCode::NOT_FOUND, format!("FACET_NOT_FOUND: {facet}\n"), head_only), _ => match registry_at(home) { Ok(registry) => { let entry = registry.entries().find(|entry| entry.short_alias && entry.served_name == facet); match entry { Some(entry) => serve_entry(home, entry, remainder, uri_path, query, head_only), None => text(StatusCode::NOT_FOUND, format!("FACET_NOT_FOUND: {facet}\n"), head_only), } } Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only), }, } } #[cfg(test)] mod tests { use super::*; use http_body_util::BodyExt; fn body(response: Response>) -> Vec { tokio::runtime::Builder::new_current_thread().build().unwrap() .block_on(response.into_body().collect()).unwrap().to_bytes().to_vec() } fn add(home: &Path, source: &Path, name: &str) -> ServedEntry { let registry_path = spt_store::perch::serving_registry_file_in(home); let mut registry = ServingRegistry::load_at(®istry_path).unwrap(); let entry = registry.add_reference(source, Some(name), None, 1).unwrap(); registry.save_at(®istry_path).unwrap(); entry } fn add_adapter(home: &Path, name: &str, alias: Option<&str>) -> ServedEntry { let root = spt_store::perch::adapter_web_dir_in(home, name).unwrap(); std::fs::create_dir_all(&root).unwrap(); let registry_path = spt_store::perch::serving_registry_file_in(home); let mut registry = ServingRegistry::load_at(®istry_path).unwrap(); let entry = registry.add_adapter(&root, name, alias, 1).unwrap(); registry.save_at(®istry_path).unwrap(); entry } fn get(home: &Path, path: &str) -> Response> { handle_path(home, &home.join("docs"), "LOCAL", 5474, path, None, false) } // [unit->REQ-WEB-URL-NODE-PREFIX] #[test] fn node_routing_reserves_facets_without_registry_fallback() { let home = tempfile::tempdir().unwrap(); for name in ["a", "m", "bin", "install", "unknown"] { let source = home.path().join(format!("{name}.txt")); std::fs::write(&source, b"reference").unwrap(); let entry = add(home.path(), &source, name); let response = get(home.path(), &format!("/LoCaL/{name}/payload")); assert_eq!(response.status(), StatusCode::NOT_FOUND); assert!(String::from_utf8(body(response)).unwrap().contains(name)); assert_eq!(body(get(home.path(), &format!("/local/f/{}", entry.served_name))), b"reference"); } let redirect = get(home.path(), "/"); assert_eq!(redirect.status(), StatusCode::FOUND); assert_eq!(redirect.headers()["location"], "/local/"); assert_eq!(get(home.path(), "/LoCaL/").status(), StatusCode::OK); let other = get(home.path(), "/OTHER/f/report.md"); assert_eq!(other.status(), StatusCode::NOT_FOUND); assert_eq!(body(other), body(serve_path(&home.path().join("docs"), "/OTHER/f/report.md", false))); } // [unit->REQ-WEB-URL-NODE-PREFIX] #[test] fn docs_aliases_preserve_existing_and_new_bundle_roots() { let home = tempfile::tempdir().unwrap(); let docs = home.path().join("docs"); std::fs::create_dir_all(docs.join("cli")).unwrap(); std::fs::create_dir_all(docs.join("new-section")).unwrap(); for (path, bytes) in [("index.html", "index"), ("cli/reference.md", "raw markdown"), ("new-section/page.html", "new docs")] { std::fs::write(docs.join(path), bytes).unwrap(); assert_eq!(body(get(home.path(), &format!("/{path}"))), bytes.as_bytes()); assert_eq!(body(get(home.path(), &format!("/local/docs/{path}"))), bytes.as_bytes()); } assert_eq!(body(get(home.path(), "/local/docs/")), b"index"); assert_eq!(body(get(home.path(), "/cli%2freference.md")), b"raw markdown"); assert_eq!(body(get(home.path(), "/local/docs/cli%2freference.md")), b"raw markdown"); assert_eq!(get(home.path(), "/cli/missing.md").status(), StatusCode::NOT_FOUND); assert_eq!(get(home.path(), "/new-section/f/resource").status(), StatusCode::NOT_FOUND); for path in ["/no-such-page.html", "/unknown/no-such-page.html"] { let actual = get(home.path(), path); let expected = serve_path(&docs, path, false); assert_eq!(actual.status(), StatusCode::NOT_FOUND); assert_eq!(actual.headers(), expected.headers()); assert_eq!(body(actual), body(expected)); } std::fs::create_dir_all(docs.join("local")).unwrap(); std::fs::write(docs.join("local/index.html"), b"shadow docs").unwrap(); assert_eq!(get(home.path(), "/local/index.html").status(), StatusCode::NOT_FOUND); assert_eq!(body(get(home.path(), "/local/docs/local/index.html")), b"shadow docs"); } // [unit->REQ-WEB-SERVING-REGISTRY] #[test] fn references_observe_edits_deletion_and_registry_removal() { let home = tempfile::tempdir().unwrap(); let source = home.path().join("source.txt"); std::fs::write(&source, b"before").unwrap(); let entry = add(home.path(), &source, "report #1.txt"); let uri = "/local/f/report%20%231.txt"; assert_eq!(body(get(home.path(), uri)), b"before"); std::fs::write(&source, b"after").unwrap(); assert_eq!(body(get(home.path(), uri)), b"after"); assert_eq!(get(home.path(), &format!("/local/f/{}", entry.id)).status(), StatusCode::NOT_FOUND); std::fs::remove_file(&source).unwrap(); assert_eq!(get(home.path(), uri).status(), StatusCode::NOT_FOUND); std::fs::write(&source, b"recreated").unwrap(); assert_eq!(body(get(home.path(), uri)), b"recreated"); let registry_path = spt_store::perch::serving_registry_file_in(home.path()); let mut registry = ServingRegistry::load_at(®istry_path).unwrap(); registry.remove(&entry.id).unwrap(); registry.save_at(®istry_path).unwrap(); assert_eq!(get(home.path(), uri).status(), StatusCode::NOT_FOUND); } // [unit->REQ-WEB-SERVING-REGISTRY] #[test] fn directory_references_reject_traversal_and_track_current_children() { let home = tempfile::tempdir().unwrap(); let source = home.path().join("shared"); std::fs::create_dir_all(source.join("nested")).unwrap(); std::fs::write(source.join("index.html"), b"directory index").unwrap(); std::fs::write(source.join("nested/item.txt"), b"child").unwrap(); std::fs::write(home.path().join("secret.txt"), b"secret").unwrap(); add(home.path(), &source, "shared"); assert_eq!(body(get(home.path(), "/local/f/shared/")), b"directory index"); assert_eq!(body(get(home.path(), "/local/f/shared/nested/item.txt")), b"child"); for path in ["/local/f/shared/../secret.txt", "/local/f/shared/%2e%2e/secret.txt", "/local/f/shared/%5c..%5csecret.txt", "/local/f/shared/c:/secret.txt", "/local/f/shared/%00", "/local/f/shared/%zz", "/local/f/shared%2fnested/item.txt"] { assert_eq!(get(home.path(), path).status(), StatusCode::BAD_REQUEST, "{path}"); } std::fs::write(source.join("nested/item.txt"), b"edited child").unwrap(); assert_eq!(body(get(home.path(), "/local/f/shared/nested/item.txt")), b"edited child"); std::fs::remove_file(source.join("nested/item.txt")).unwrap(); assert_eq!(get(home.path(), "/local/f/shared/nested/item.txt").status(), StatusCode::NOT_FOUND); std::fs::remove_dir_all(source).unwrap(); assert_eq!(get(home.path(), "/local/f/shared/").status(), StatusCode::NOT_FOUND); } // [unit->REQ-WEB-SERVING-REGISTRY] #[cfg(unix)] #[test] fn directory_symlinks_cannot_escape_the_current_served_root() { let home = tempfile::tempdir().unwrap(); let source = home.path().join("shared"); std::fs::create_dir(&source).unwrap(); std::fs::write(source.join("inside.txt"), b"inside").unwrap(); std::fs::write(home.path().join("secret.txt"), b"secret").unwrap(); std::os::unix::fs::symlink(source.join("inside.txt"), source.join("allowed.txt")).unwrap(); std::os::unix::fs::symlink(home.path().join("secret.txt"), source.join("escape.txt")).unwrap(); std::os::unix::fs::symlink(home.path(), source.join("escape-dir")).unwrap(); add(home.path(), &source, "shared"); assert_eq!(body(get(home.path(), "/local/f/shared/allowed.txt")), b"inside"); assert_eq!(get(home.path(), "/local/f/shared/escape.txt").status(), StatusCode::NOT_FOUND); assert_eq!(get(home.path(), "/local/f/shared/escape-dir/secret.txt").status(), StatusCode::NOT_FOUND); } // [unit->REQ-WEB-SERVING-REGISTRY] #[test] fn adapter_facet_and_disambiguated_alias_share_one_contained_reference() { let home = tempfile::tempdir().unwrap(); let occupied = home.path().join("occupied.txt"); std::fs::write(&occupied, b"first registrant").unwrap(); add(home.path(), &occupied, "short"); let entry = add_adapter(home.path(), "example", Some("short")); assert_eq!(entry.served_name, "short~1"); std::fs::write(entry.path.join("output.txt"), b"adapter output").unwrap(); std::fs::write(entry.path.parent().unwrap().join("private.txt"), b"private").unwrap(); for prefix in ["/local/a/example", "/local/short~1"] { assert_eq!(body(get(home.path(), &format!("{prefix}/output.txt"))), b"adapter output"); assert_eq!(get(home.path(), &format!("{prefix}/../private.txt")).status(), StatusCode::BAD_REQUEST); assert_eq!(get(home.path(), &format!("{prefix}/%2e%2e/private.txt")).status(), StatusCode::BAD_REQUEST); } std::fs::write(entry.path.join("output.txt"), b"edited output").unwrap(); assert_eq!(body(get(home.path(), "/local/a/example/output.txt")), b"edited output"); assert_eq!(body(get(home.path(), "/local/short~1/output.txt")), b"edited output"); assert_eq!(body(get(home.path(), "/local/f/short")), b"first registrant"); assert_eq!(get(home.path(), "/local/short").status(), StatusCode::NOT_FOUND); let response = handle_path(home.path(), &home.path().join("docs"), "LOCAL", 5474, "/local/", Some("json"), false); let json: serde_json::Value = serde_json::from_slice(&body(response)).unwrap(); let rows: Vec<_> = json["entries"].as_array().unwrap().iter().filter(|row| row["adapter"] == "example").collect(); assert_eq!(rows.len(), 1, "facet and alias must not duplicate exposure"); assert_eq!(rows[0]["url"], "http://localhost:5474/local/a/example/"); assert_eq!(rows[0]["alias_url"], "http://localhost:5474/local/short~1/"); let no_alias = add_adapter(home.path(), "plain", None); std::fs::write(no_alias.path.join("output.txt"), b"facet only").unwrap(); assert_eq!(body(get(home.path(), "/local/a/plain/output.txt")), b"facet only"); assert_eq!(get(home.path(), &format!("/local/{}/output.txt", no_alias.served_name)).status(), StatusCode::NOT_FOUND); std::fs::remove_file(entry.path.join("output.txt")).unwrap(); assert_eq!(get(home.path(), "/local/a/example/output.txt").status(), StatusCode::NOT_FOUND); assert_eq!(get(home.path(), "/local/short~1/output.txt").status(), StatusCode::NOT_FOUND); } // [unit->REQ-WEB-SERVING-REGISTRY] #[cfg(unix)] #[test] fn adapter_facet_and_alias_refuse_symlink_escape() { let home = tempfile::tempdir().unwrap(); let entry = add_adapter(home.path(), "example", Some("short")); std::fs::write(home.path().join("private.txt"), b"private").unwrap(); std::os::unix::fs::symlink(home.path(), entry.path.join("escape")).unwrap(); for path in ["/local/a/example/escape/private.txt", "/local/short/escape/private.txt"] { assert_eq!(get(home.path(), path).status(), StatusCode::NOT_FOUND); } } // [unit->REQ-WEB-SERVING-REGISTRY] #[cfg(unix)] #[test] fn adapter_root_replacement_cannot_expose_an_install_tree() { let home = tempfile::tempdir().unwrap(); let entry = add_adapter(home.path(), "example", Some("short")); let install = home.path().join("install"); std::fs::create_dir(&install).unwrap(); std::fs::write(install.join("manifest.toml"), b"private install bytes").unwrap(); std::fs::write(entry.path.join("manifest.toml"), b"deliberately served bytes").unwrap(); let paths = ["/local/a/example/manifest.toml", "/local/short/manifest.toml"]; for path in paths { assert_eq!(body(get(home.path(), path)), b"deliberately served bytes"); } std::fs::remove_dir_all(&entry.path).unwrap(); std::os::unix::fs::symlink(&install, &entry.path).unwrap(); for path in paths { assert_eq!(get(home.path(), path).status(), StatusCode::NOT_FOUND); } // Removing the redirect and restoring the core-owned root restores // the same registered exposure without a registry rewrite. std::fs::remove_file(&entry.path).unwrap(); std::fs::create_dir(&entry.path).unwrap(); std::fs::write(entry.path.join("manifest.toml"), b"restored output").unwrap(); for path in paths { assert_eq!(body(get(home.path(), path)), b"restored output"); } } // [unit->REQ-WEB-URL-NODE-PREFIX] #[test] fn index_escapes_html_encodes_urls_and_reports_real_source_paths() { let home = tempfile::tempdir().unwrap(); let source = home.path().join("source & quote.txt"); std::fs::write(&source, b"source").unwrap(); let entry = add(home.path(), &source, "report & #1.txt"); let html = String::from_utf8(body(get(home.path(), "/local/"))).unwrap(); assert!(html.contains("source & quote.txt")); assert!(html.contains("/local/f/report%20%26%20%231.txt")); let node = "