Parent [>]
1//! Route handler for serving raw file contents from a git repository.
2//!
3//! Covers one route: `/{repo}/commits/{sha}/raw/{*path}`.  The handler resolves
4//! the rev-spec `{sha}:{path}` via `gix`, validates the path against directory
5//! traversal, and serves the blob bytes with an appropriate `Content-Type`
6//! header derived from the file extension.
7
8use axum::extract::{Path, State};
9use axum::http::header;
10use axum::response::IntoResponse;
11use serde::Deserialize;
12
13use crate::error::{Error, Result};
14use crate::git::{self, GitResultExt as _};
15use crate::routes::{AppState, RepoName};
16
17/// Path parameters for the raw file route.
18#[derive(Deserialize)]
19pub(super) struct RawPath {
20    sha: String,
21    path: String,
22}
23
24/// Serves the raw contents of a file at `{sha}:{path}` in the given repository
25/// with an appropriate `Content-Type` header.
26pub async fn handler(
27    State(state): State<AppState>,
28    RepoName(name): RepoName,
29    Path(params): Path<RawPath>,
30) -> Result<impl IntoResponse> {
31    git::validate_path(&params.path)?;
32
33    let git_repo = git::open_repo(&state.root, &name)?;
34
35    // Build a `sha:path` rev-spec and resolve it to a blob.
36    let spec = format!("{}:{}", params.sha, params.path);
37    let blob = git_repo
38        .rev_parse_single(spec.as_str())
39        .map_err(|_| Error::NotFound(spec.clone()))?
40        .object()
41        .corrupt()?
42        .try_into_blob()
43        .map_err(|_| Error::NotFound(spec))?;
44
45    let content_type = content_type_for(&params.path);
46
47    Ok((
48        [
49            (header::CONTENT_TYPE, content_type),
50            (header::CONTENT_SECURITY_POLICY, "sandbox"),
51            (header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
52        ],
53        blob.data.clone(),
54    ))
55}
56
57/// Returns a `Content-Type` string based on the file extension of `path`.
58/// Falls back to `application/octet-stream` for unknown extensions.
59fn content_type_for(path: &str) -> &'static str {
60    match path.rsplit('.').next().unwrap_or("") {
61        "png" => "image/png",
62        "jpg" | "jpeg" => "image/jpeg",
63        "gif" => "image/gif",
64        "svg" => "image/svg+xml",
65        "webp" => "image/webp",
66        "ico" => "image/x-icon",
67        "txt" | "md" => "text/plain; charset=utf-8",
68        _ => "application/octet-stream",
69    }
70}