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).

![logo](docs/logo.png)

<img src=\"docs/logo.png\" width=\"100\">
";

const GUIDE: &str = "# Guide\n\nBack to the [README](../README.md); see the [logo](logo.png) and ![it](logo.png).\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 &lt;2026&gt;"));
    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);
}