a3s 0.8.2

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
use std::path::{Path, PathBuf};
use std::sync::Arc;

use axum::body::Body;
use axum::http::{
    header::{CACHE_CONTROL, CONTENT_TYPE},
    HeaderValue, StatusCode, Uri,
};
use axum::response::Response;

pub(in crate::api) async fn api_only_fallback() -> Response {
    response_with_status(
        StatusCode::NOT_FOUND,
        "text/plain; charset=utf-8",
        "A3S Code API is running. Web assets are disabled.",
    )
}

pub(in crate::api) async fn serve_static(uri: Uri, root: Arc<PathBuf>) -> Response {
    let Some(path) = static_path(&root, uri.path()) else {
        return response_with_status(
            StatusCode::BAD_REQUEST,
            "text/plain; charset=utf-8",
            "invalid static path",
        );
    };

    match tokio::fs::read(&path).await {
        Ok(body) => response_with_headers(StatusCode::OK, content_type_for(&path), body),
        Err(_) => response_with_status(
            StatusCode::NOT_FOUND,
            "text/plain; charset=utf-8",
            "not found",
        ),
    }
}

fn static_path(root: &Path, request_path: &str) -> Option<PathBuf> {
    let trimmed = request_path.trim_start_matches('/');
    let mut candidate = root.to_path_buf();
    if !trimmed.is_empty() {
        for segment in trimmed.split('/') {
            if segment.is_empty() || segment == "." {
                continue;
            }
            if segment == ".." || segment.contains('\\') {
                return None;
            }
            candidate.push(segment);
        }
    }

    if candidate.is_dir() {
        return Some(candidate.join("index.html"));
    }
    if candidate.is_file() {
        return Some(candidate);
    }
    Some(root.join("index.html"))
}

fn content_type_for(path: &Path) -> &'static str {
    match path.extension().and_then(|ext| ext.to_str()).unwrap_or("") {
        "css" => "text/css; charset=utf-8",
        "html" => "text/html; charset=utf-8",
        "js" | "mjs" => "text/javascript; charset=utf-8",
        "json" => "application/json",
        "png" => "image/png",
        "svg" => "image/svg+xml",
        "webp" => "image/webp",
        "ico" => "image/x-icon",
        "wasm" => "application/wasm",
        _ => "application/octet-stream",
    }
}

fn response_with_status(
    status: StatusCode,
    content_type: &'static str,
    body: &'static str,
) -> Response {
    response_with_headers(status, content_type, body.as_bytes().to_vec())
}

fn response_with_headers(
    status: StatusCode,
    content_type: &'static str,
    body: Vec<u8>,
) -> Response {
    let mut response = Response::new(Body::from(body));
    *response.status_mut() = status;
    response
        .headers_mut()
        .insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
    response.headers_mut().insert(
        CACHE_CONTROL,
        HeaderValue::from_static("no-cache, no-store, must-revalidate"),
    );
    response
}