Change PDF rendering
This commit is contained in:
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user