files.rs
raw
//! Browsing files on the default branch, and the links a README's relative
//! references turn into.
mod common;
use assert2::check;
use common::Fixture;
use poem::{http::StatusCode, test::TestClient};
const PNG: &[u8] = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR";
const README: &str = "# Widget
See the [license](LICENSE), the [guide](docs/guide.md), the [docs folder](docs/),
the [root license](/LICENSE), the [spaced file](docs/my%20file.md),
an [external site](https://example.com/x), [mail](mailto:a@b.c) and [usage](#usage).

<img src=\"docs/logo.png\" width=\"100\">
";
const GUIDE: &str = "# Guide\n\nBack to the [README](../README.md); see the [logo](logo.png) and .\n";
fn fixture() -> Fixture {
let fx = Fixture::new();
fx.repo_bytes(
"widget",
true,
"Widgets",
&[
("README.md", README.as_bytes()),
("LICENSE", b"MIT License <2026>\n"),
("docs/guide.md", GUIDE.as_bytes()),
("docs/my file.md", b"# Spaced\n"),
("docs/logo.png", PNG),
("docs/index.html", b"<script>alert(1)</script>"),
("src/main.rs", b"fn main() {}\n"),
],
);
fx.repo("secret", false, "Secret", &[("LICENSE", "no\n")]);
fx
}
async fn text(client: &TestClient<impl poem::Endpoint>, path: &str) -> (StatusCode, String) {
let resp = client.get(path).send().await;
let status = resp.0.status();
(status, resp.0.into_body().into_string().await.unwrap())
}
#[tokio::test]
async fn readme_links_point_into_the_file_and_raw_views() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let (status, body) = text(&client, "/widget").await;
check!(status == StatusCode::OK);
check!(body.contains("href=\"/widget/file/LICENSE\""));
check!(body.contains("href=\"/widget/file/docs/guide.md\""));
check!(body.contains("href=\"/widget/file/docs\""));
check!(body.contains("href=\"/widget/file/docs/my%20file.md\""));
check!(body.contains("src=\"/widget/raw/docs/logo.png\""));
check!(body.contains("src=\"/widget/raw/docs/logo.png\" width=\"100\""));
check!(body.contains("href=\"https://example.com/x\""));
check!(body.contains("href=\"mailto:a@b.c\""));
check!(body.contains("href=\"#user-content-usage\""));
check!(body.matches("href=\"/widget/file/LICENSE\"").count() == 2);
}
#[tokio::test]
async fn file_view_shows_text_markdown_and_images() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let (status, body) = text(&client, "/widget/file/LICENSE").await;
check!(status == StatusCode::OK);
check!(body.contains("MIT License <2026>"));
check!(body.contains("href=\"/widget/raw/LICENSE\""));
let (status, body) = text(&client, "/widget/file/docs/guide.md").await;
check!(status == StatusCode::OK);
check!(body.contains("<h1>Guide</h1>"));
check!(body.contains("href=\"/widget/file/README.md\""));
check!(body.contains("href=\"/widget/file/docs/logo.png\""));
check!(body.contains("src=\"/widget/raw/docs/logo.png\""));
let (status, body) = text(&client, "/widget/file/docs/logo.png").await;
check!(status == StatusCode::OK);
check!(body.contains("<img src=\"/widget/raw/docs/logo.png\""));
let (status, _) = text(&client, "/widget/file/docs/my%20file.md").await;
check!(status == StatusCode::OK);
}
#[tokio::test]
async fn file_view_lists_directories_with_breadcrumbs() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let (status, body) = text(&client, "/widget/file").await;
check!(status == StatusCode::OK);
check!(body.contains("href=\"/widget/file/docs\">docs/</a>"));
check!(body.contains("href=\"/widget/file/README.md\">README.md</a>"));
check!(
body.find("docs/</a>") < body.find("LICENSE</a>"),
"directories come first"
);
let (status, body) = text(&client, "/widget/file/docs/").await;
check!(status == StatusCode::OK);
check!(body.contains("href=\"/widget/file/docs/guide.md\">guide.md</a>"));
check!(body.contains("href=\"/widget/file\">files</a>"));
check!(body.contains("<span class=\"current\">docs</span>"));
}
#[tokio::test]
async fn unreadable_readme_offers_its_raw_bytes() {
let fx = Fixture::new();
fx.repo_bytes("latin", true, "", &[("README", b"Caf\xe9\n")]);
let client = TestClient::new(fx.app.routes());
let (status, body) = text(&client, "/latin").await;
check!(status == StatusCode::OK);
check!(body.contains("binary file"));
check!(body.contains("href=\"/latin/raw/README\""));
}
#[tokio::test]
async fn submodules_are_neither_listed_nor_served() {
let fx = fixture();
let path = fx.dir.path().join("widget.git");
let repo = git2::Repository::open_bare(&path).expect("open");
let head = repo.head().unwrap().peel_to_commit().unwrap();
let mut builder = repo.treebuilder(Some(&head.tree().unwrap())).unwrap();
let elsewhere = git2::Oid::from_str("0123456789abcdef0123456789abcdef01234567").unwrap();
builder.insert("vendor", elsewhere, 0o160000).unwrap();
let tree = repo.find_tree(builder.write().unwrap()).unwrap();
let sig = git2::Signature::now("Test", "test@example.com").unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "submodule", &tree, &[&head])
.unwrap();
let client = TestClient::new(fx.app.routes());
let (status, body) = text(&client, "/widget/file").await;
check!(status == StatusCode::OK);
check!(!body.contains("vendor"));
for path in ["/widget/file/vendor", "/widget/raw/vendor"] {
let (status, _) = text(&client, path).await;
check!(status == StatusCode::NOT_FOUND, "{path}");
}
}
#[tokio::test]
async fn raw_view_serves_bytes_with_safe_media_types() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let resp = client.get("/widget/raw/docs/logo.png").send().await;
resp.assert_status_is_ok();
resp.assert_content_type("image/png");
resp.assert_header("x-content-type-options", "nosniff");
resp.assert_header("content-security-policy", "sandbox");
check!(resp.0.into_body().into_vec().await.unwrap() == PNG);
let resp = client.get("/widget/raw/LICENSE").send().await;
resp.assert_status_is_ok();
resp.assert_content_type("text/plain; charset=utf-8");
resp.assert_header("content-length", "19");
let resp = client.get("/widget/raw/docs/index.html").send().await;
resp.assert_status_is_ok();
resp.assert_content_type("text/plain; charset=utf-8");
}
#[tokio::test]
async fn nothing_leaks_from_private_or_invalid_paths() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
for path in [
"/secret/file",
"/secret/file/LICENSE",
"/secret/raw/LICENSE",
"/widget/file/nope.txt",
"/widget/raw/docs",
"/widget/raw/nope.txt",
"/widget/file/../secret",
"/widget/raw/..%2FLICENSE",
] {
let resp = client.get(path).send().await;
check!(resp.0.status() == StatusCode::NOT_FOUND, "{path}");
}
}
#[tokio::test]
async fn raw_view_streams_a_file_too_large_for_a_page() {
let fx = Fixture::new();
let big: Vec<u8> = (0..3 * 1024 * 1024).map(|i| (i % 251) as u8).collect();
fx.repo_bytes("big", true, "Big", &[("blob.bin", &big)]);
let client = TestClient::new(fx.app.routes());
let resp = client.get("/big/raw/blob.bin").send().await;
resp.assert_status_is_ok();
resp.assert_content_type("application/octet-stream");
resp.assert_header("content-length", &big.len().to_string());
let body = resp.0.into_body().into_vec().await.unwrap();
check!(body == big);
}