Parent [>]
1mod archive;
2mod blame;
3mod branch;
4mod commit;
5mod raw;
6mod repo;
7mod tag;
8mod tree;
9
10use std::collections::HashMap;
11use std::path::{Component, PathBuf};
12
13use crate::error::{Error, Result};
14use axum::Router;
15use axum::extract::{FromRequestParts, Path, State};
16use axum::http::header;
17use axum::http::request::Parts;
18use axum::response::IntoResponse;
19use axum::routing::get;
20use serde::Serialize;
21
22/// A breadcrumb segment for the navigation bar.
23#[derive(Serialize)]
24pub struct Breadcrumb {
25    pub label: String,
26    pub url: String,
27}
28
29/// Builds breadcrumbs for a tree path.
30///
31/// The root breadcrumb is always `"tree"`.  Each path segment adds a breadcrumb
32/// linking to its corresponding subdirectory.
33pub fn build_breadcrumbs(base: &str, repo: &str, sha: &str, path: &str) -> Vec<Breadcrumb> {
34    let mut crumbs = Vec::new();
35    crumbs.push(Breadcrumb {
36        label: "tree".to_string(),
37        url: format!("{base}/{repo}/commits/{sha}/tree"),
38    });
39    if !path.is_empty() {
40        let mut accumulated = String::new();
41        for segment in path.split('/') {
42            if !accumulated.is_empty() {
43                accumulated.push('/');
44            }
45            accumulated.push_str(segment);
46            crumbs.push(Breadcrumb {
47                label: segment.to_string(),
48                url: format!("{base}/{repo}/commits/{sha}/tree/{accumulated}"),
49            });
50        }
51    }
52    crumbs
53}
54
55/// Axum extractor that pulls the `{repo}` path parameter and validates it
56/// contains no path traversal components before passing it to handlers.
57pub struct RepoName(pub String);
58
59impl<S: Send + Sync> FromRequestParts<S> for RepoName {
60    type Rejection = Error;
61
62    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self> {
63        let Path(params) = Path::<HashMap<String, String>>::from_request_parts(parts, state)
64            .await
65            .map_err(|e| Error::BadRequest(e.to_string()))?;
66
67        let name = params
68            .get("repo")
69            .ok_or_else(|| Error::BadRequest("missing repo parameter".to_string()))?
70            .clone();
71
72        // Reject names containing non-normal components (e.g. `..`) to prevent
73        // directory traversal.
74        if std::path::Path::new(&name)
75            .components()
76            .any(|c| !matches!(c, Component::Normal(_)))
77        {
78            return Err(Error::BadRequest("invalid repository name".to_string()));
79        }
80
81        Ok(Self(name))
82    }
83}
84
85/// Shared application state passed to all route handlers.
86#[derive(Clone)]
87pub struct AppState {
88    /// Path to the directory containing bare git repositories.
89    pub root: PathBuf,
90    /// Concatenated CSS served at `/style.css`.
91    pub css: String,
92}
93
94/// Serves the concatenated stylesheet.
95async fn style(State(state): State<AppState>) -> impl IntoResponse {
96    (
97        [(header::CONTENT_TYPE, "text/css; charset=utf-8")],
98        state.css,
99    )
100}
101
102/// Serves the favicon (SVG or ICO bytes loaded at startup).
103async fn favicon() -> impl IntoResponse {
104    (
105        [(header::CONTENT_TYPE, "image/svg+xml")],
106        crate::config::favicon(),
107    )
108}
109
110/// Builds the application router with all routes and shared state attached.
111pub fn router(state: AppState) -> Router {
112    Router::new()
113        .route("/favicon.ico", get(favicon))
114        .route("/style.css", get(style))
115        .route("/", get(repo::list))
116        .route("/{repo}", get(repo::detail))
117        .route("/{repo}/tags", get(tag::list))
118        .route("/{repo}/tags/{*tag}", get(tag::detail))
119        .route("/{repo}/branches", get(branch::list))
120        .route("/{repo}/branches/{*branch}", get(branch::detail))
121        .route("/{repo}/commits", get(commit::list))
122        .route("/{repo}/commits/{sha}", get(commit::detail))
123        .route("/{repo}/commits/{sha}/tree", get(tree::handler))
124        .route("/{repo}/commits/{sha}/tree/{*path}", get(tree::handler))
125        .route("/{repo}/commits/{sha}/blame/{*path}", get(blame::handler))
126        .route("/{repo}/commits/{sha}/raw/{*path}", get(raw::handler))
127        .route("/{repo}/archive/{format}/{*ref}", get(archive::handler))
128        .with_state(state)
129}