//! Custom `brittle://` URI scheme handler. //! //! Routes: //! `brittle://app/viewer?ref_id=` — the PDF viewer HTML page //! `brittle://app/pdfjs/` — pdfjs-dist static assets //! `brittle://app/pdf?ref_id=` — raw PDF bytes from the repository //! //! The pure routing and path-resolution logic lives in the `routing` sub-module //! so it can be unit-tested without a running Tauri application. use std::path::PathBuf; use tauri::{ http::{header, Request, Response, StatusCode}, AppHandle, Manager, Runtime, }; use crate::state::AppState; // ── Embedded assets ─────────────────────────────────────────────────────────── static VIEWER_HTML: &[u8] = include_bytes!("pdf_viewer.html"); static PDFJS_MIN_JS: &[u8] = include_bytes!( "../pdfjs/node_modules/pdfjs-dist/build/pdf.min.js" ); static PDFJS_WORKER_JS: &[u8] = include_bytes!( "../pdfjs/node_modules/pdfjs-dist/build/pdf.worker.min.js" ); // ── Public API ──────────────────────────────────────────────────────────────── /// Entry point registered with `tauri::Builder::register_uri_scheme_protocol`. /// /// Generic over the Tauri runtime so the function can be used from a closure /// in the builder without knowing the concrete runtime at compile time. pub fn handle(app: &AppHandle, req: &Request>) -> Response> { let uri = req.uri(); match routing::classify(uri.path(), uri.query()) { routing::Route::Viewer { ref_id } => serve_viewer(&ref_id), routing::Route::PdfjsAsset { rel_path } => { if rel_path.contains("..") { return response_403(); } serve_pdfjs_file(&rel_path) } routing::Route::Pdf { ref_id } => serve_pdf(app, &ref_id), routing::Route::NotFound => response_404(), } } // ── Route handlers ──────────────────────────────────────────────────────────── fn serve_viewer(_ref_id: &str) -> Response> { // The viewer reads `ref_id` from its own URL query string via JavaScript // (`new URLSearchParams(location.search).get("ref_id")`), so no substitution // into the HTML template is needed. response_ok(VIEWER_HTML.to_vec(), "text/html; charset=utf-8") } fn serve_pdfjs_file(rel_path: &str) -> Response> { let bytes: &[u8] = match rel_path { "build/pdf.min.js" => PDFJS_MIN_JS, "build/pdf.worker.min.js" => PDFJS_WORKER_JS, _ => return response_404(), }; let mut resp = response_ok(bytes.to_vec(), "application/javascript; charset=utf-8"); resp.headers_mut().insert( header::CACHE_CONTROL, "public, max-age=3600".parse().unwrap(), ); resp } fn serve_pdf(app: &AppHandle, ref_id: &str) -> Response> { use brittle_core::{model::ids::ReferenceId, store::FsStore, Brittle}; use uuid::Uuid; let uuid = match Uuid::parse_str(ref_id) { Ok(u) => u, Err(_) => return response_400("invalid ref_id: not a valid UUID"), }; let rid = ReferenceId::from(uuid); let state = app.state::(); let pdf_path: Result = state.with_repo_read(|b: &Brittle| b.get_pdf_path(rid)); match pdf_path { Err(e) => response_404_msg(&e), Ok(path) => match std::fs::read(&path) { Ok(bytes) => { let mut resp = response_ok(bytes, "application/pdf"); resp.headers_mut() .insert(header::CACHE_CONTROL, "no-store".parse().unwrap()); resp } Err(e) => response_500(&e.to_string()), }, } } // ── Response builders ───────────────────────────────────────────────────────── fn response_ok(body: Vec, content_type: &str) -> Response> { Response::builder() .header(header::CONTENT_TYPE, content_type) .header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") .status(StatusCode::OK) .body(body) .unwrap() } fn response_404() -> Response> { Response::builder() .status(StatusCode::NOT_FOUND) .body(b"Not Found".to_vec()) .unwrap() } fn response_404_msg(msg: &str) -> Response> { Response::builder() .status(StatusCode::NOT_FOUND) .body(msg.as_bytes().to_vec()) .unwrap() } fn response_400(msg: &str) -> Response> { Response::builder() .status(StatusCode::BAD_REQUEST) .body(msg.as_bytes().to_vec()) .unwrap() } fn response_403() -> Response> { Response::builder() .status(StatusCode::FORBIDDEN) .body(b"Forbidden".to_vec()) .unwrap() } fn response_500(msg: &str) -> Response> { Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(msg.as_bytes().to_vec()) .unwrap() } // ── Pure routing logic (unit-testable) ─────────────────────────────────────── pub mod routing { #[derive(Debug, PartialEq)] pub enum Route { Viewer { ref_id: String }, PdfjsAsset { rel_path: String }, Pdf { ref_id: String }, NotFound, } /// Classify a `brittle://app/{path}?{query}` request into a `Route`. pub fn classify(path: &str, query: Option<&str>) -> Route { let ref_id = extract_ref_id(query); if path == "/viewer" { Route::Viewer { ref_id } } else if let Some(rel) = path.strip_prefix("/pdfjs/") { Route::PdfjsAsset { rel_path: rel.to_owned(), } } else if path == "/pdf" { Route::Pdf { ref_id } } else { Route::NotFound } } /// Extract the value of `ref_id=…` from a URL query string. pub fn extract_ref_id(query: Option<&str>) -> String { query .unwrap_or("") .split('&') .find_map(|part| part.strip_prefix("ref_id=")) .unwrap_or("") .to_owned() } #[cfg(test)] mod tests { use super::*; #[test] fn viewer_route() { let r = classify("/viewer", Some("ref_id=abc-123")); assert_eq!( r, Route::Viewer { ref_id: "abc-123".into() } ); } #[test] fn viewer_route_no_ref_id() { let r = classify("/viewer", None); assert_eq!(r, Route::Viewer { ref_id: "".into() }); } #[test] fn pdfjs_asset_route_build_file() { let r = classify("/pdfjs/build/pdf.min.js", None); assert_eq!( r, Route::PdfjsAsset { rel_path: "build/pdf.min.js".into() } ); } #[test] fn pdfjs_asset_route_nested() { let r = classify("/pdfjs/web/pdf_viewer.css", None); assert_eq!( r, Route::PdfjsAsset { rel_path: "web/pdf_viewer.css".into() } ); } #[test] fn pdf_route() { let r = classify("/pdf", Some("ref_id=01234567-89ab-cdef-0123-456789abcdef")); assert_eq!( r, Route::Pdf { ref_id: "01234567-89ab-cdef-0123-456789abcdef".into() } ); } #[test] fn unknown_paths_are_not_found() { assert_eq!(classify("/unknown", None), Route::NotFound); assert_eq!(classify("/", None), Route::NotFound); assert_eq!(classify("/favicon.ico", None), Route::NotFound); } #[test] fn extract_ref_id_from_compound_query() { let id = extract_ref_id(Some("foo=bar&ref_id=my-id&baz=1")); assert_eq!(id, "my-id"); } #[test] fn extract_ref_id_missing_returns_empty() { assert_eq!(extract_ref_id(None), ""); assert_eq!(extract_ref_id(Some("foo=bar")), ""); } #[test] fn path_traversal_rel_path_contains_dotdot() { // The handler rejects rel_paths containing ".."; verify the routing // surfaces them so the handler can block them. if let Route::PdfjsAsset { rel_path } = classify("/pdfjs/../secret", None) { assert!(rel_path.contains("..")); } else { panic!("expected PdfjsAsset route"); } } } }