Change PDF rendering

This commit is contained in:
2026-03-30 00:03:19 +02:00
parent 7f9d766ce0
commit d1bb79570d
9 changed files with 891 additions and 307 deletions

View File

@@ -201,6 +201,7 @@ mod tests {
let original = LayoutConfig {
left_pane_fraction: 0.25,
right_pane_fraction: 0.40,
..LayoutConfig::default()
};
let s = toml::to_string_pretty(&original).unwrap();
let parsed: LayoutConfig = toml::from_str(&s).unwrap();
@@ -286,6 +287,7 @@ mod tests {
layout: LayoutConfig {
left_pane_fraction: 0.30,
right_pane_fraction: 0.40,
..LayoutConfig::default()
},
..Default::default()
};

View File

@@ -26,6 +26,14 @@ static PDFJS_WORKER_JS: &[u8] = include_bytes!(
"../assets/viewer/pdfjs/pdf.worker.min.js"
);
// Viewer JS modules (served at brittle://app/viewer/<file>)
static VIEWER_JS: &[u8] = include_bytes!("../assets/viewer/viewer.js");
static VIEWPORT_TRACKER_JS: &[u8] = include_bytes!("../assets/viewer/viewport-tracker.js");
static PAGE_MANAGER_JS: &[u8] = include_bytes!("../assets/viewer/page-manager.js");
static ZOOM_CONTROLLER_JS: &[u8] = include_bytes!("../assets/viewer/zoom-controller.js");
static RENDER_WORKER_JS: &[u8] = include_bytes!("../assets/viewer/render-worker.js");
static MESSAGE_BRIDGE_JS: &[u8] = include_bytes!("../assets/viewer/message-bridge.js");
// ── Public API ────────────────────────────────────────────────────────────────
/// Entry point registered with `tauri::Builder::register_uri_scheme_protocol`.
@@ -37,6 +45,7 @@ pub fn handle<R: Runtime>(app: &AppHandle<R>, req: &Request<Vec<u8>>) -> Respons
match routing::classify(uri.path(), uri.query()) {
routing::Route::Viewer { ref_id } => serve_viewer(&ref_id),
routing::Route::ViewerAsset { file_name } => serve_viewer_asset(&file_name),
routing::Route::PdfjsAsset { rel_path } => {
if rel_path.contains("..") {
return response_403();
@@ -57,6 +66,19 @@ fn serve_viewer(_ref_id: &str) -> Response<Vec<u8>> {
response_ok(VIEWER_HTML.to_vec(), "text/html; charset=utf-8")
}
fn serve_viewer_asset(file_name: &str) -> Response<Vec<u8>> {
let bytes: &[u8] = match file_name {
"viewer.js" => VIEWER_JS,
"viewport-tracker.js" => VIEWPORT_TRACKER_JS,
"page-manager.js" => PAGE_MANAGER_JS,
"zoom-controller.js" => ZOOM_CONTROLLER_JS,
"render-worker.js" => RENDER_WORKER_JS,
"message-bridge.js" => MESSAGE_BRIDGE_JS,
_ => return response_404(),
};
response_ok(bytes.to_vec(), "application/javascript; charset=utf-8")
}
fn serve_pdfjs_file(rel_path: &str) -> Response<Vec<u8>> {
let bytes: &[u8] = match rel_path {
"build/pdf.min.js" => PDFJS_MIN_JS,
@@ -152,17 +174,31 @@ pub mod routing {
#[derive(Debug, PartialEq)]
pub enum Route {
Viewer { ref_id: String },
ViewerAsset { file_name: String },
PdfjsAsset { rel_path: String },
Pdf { ref_id: String },
NotFound,
}
/// Classify a `brittle://app/{path}?{query}` request into a `Route`.
///
/// - `/viewer` (no further path segments) → HTML shell
/// - `/viewer/<file>` (one path segment) → viewer JS module
/// - `/pdfjs/<rel>` → PDF.js asset
/// - `/pdf` → raw PDF bytes
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(rest) = path.strip_prefix("/viewer/") {
// Reject any path that contains directory separators or dots
// at the start, to prevent traversal.
if rest.contains('/') || rest.starts_with('.') {
Route::NotFound
} else {
Route::ViewerAsset { file_name: rest.to_owned() }
}
} else if let Some(rel) = path.strip_prefix("/pdfjs/") {
Route::PdfjsAsset {
rel_path: rel.to_owned(),
@@ -239,6 +275,37 @@ pub mod routing {
);
}
#[test]
fn viewer_asset_route() {
let r = classify("/viewer/viewer.js", None);
assert_eq!(r, Route::ViewerAsset { file_name: "viewer.js".into() });
}
#[test]
fn viewer_asset_route_all_modules() {
for name in &[
"viewer.js",
"viewport-tracker.js",
"page-manager.js",
"zoom-controller.js",
"render-worker.js",
"message-bridge.js",
] {
let path = format!("/viewer/{}", name);
assert_eq!(
classify(&path, None),
Route::ViewerAsset { file_name: name.to_string() }
);
}
}
#[test]
fn viewer_asset_traversal_blocked() {
// Directory traversal via nested path → NotFound
assert_eq!(classify("/viewer/../secret.js", None), Route::NotFound);
assert_eq!(classify("/viewer/sub/file.js", None), Route::NotFound);
}
#[test]
fn unknown_paths_are_not_found() {
assert_eq!(classify("/unknown", None), Route::NotFound);