diff --git a/.changes/fix-build-without-x11.md b/.changes/fix-build-without-x11.md new file mode 100644 index 0000000..0b7df59 --- /dev/null +++ b/.changes/fix-build-without-x11.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Fix the build when not enabling the `x11` feature. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 097ec72..204f401 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,12 @@ jobs: - { target: aarch64-apple-ios, os: macos-latest } - { target: x86_64-apple-darwin, os: macos-latest } - { target: aarch64-linux-android, os: ubuntu-latest } + features: + - { + args: --no-default-features --features os-webview, + key: no-default, + } + - { args: --all-features, key: all } runs-on: ${{ matrix.platform.os }} @@ -42,18 +48,20 @@ jobs: cmd /C start /wait installwebview.exe /silent /install - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ matrix.features.key == 'all' }} - name: build wry - run: cargo build --target ${{ matrix.platform.target }} --all-features + run: cargo build --target ${{ matrix.platform.target }} ${{ matrix.features.args }} - name: build tests and examples shell: bash if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios')) - run: cargo test --no-run --verbose --target ${{ matrix.platform.target }} + run: cargo test --no-run --verbose --target ${{ matrix.platform.target }} ${{ matrix.features.args }} - name: run tests if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios')) - run: cargo test --verbose --target ${{ matrix.platform.target }} --features linux-body + run: cargo test --verbose --target ${{ matrix.platform.target }} --features linux-body ${{ matrix.features.args }} - name: install nightly uses: dtolnay/rust-toolchain@nightly @@ -63,7 +71,7 @@ jobs: - name: Run tests with miri if: (!contains(matrix.platform.target, 'android') && !contains(matrix.platform.target, 'ios')) - run: cargo +nightly miri test --verbose --target ${{ matrix.platform.target }} --features linux-body + run: cargo +nightly miri test --verbose --target ${{ matrix.platform.target }} --features linux-body ${{ matrix.features.args }} doc: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 8da83a0..7c1e25e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,9 @@ gdkx11 = { version = "0.18", optional = true } percent-encoding = "2.3" dirs = "6" +[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dev-dependencies] +x11-dl = { version = "2.21" } + [target."cfg(target_os = \"windows\")".dependencies] webview2-com = "0.38" windows-version = "0.1" diff --git a/examples/async_custom_protocol.rs b/examples/async_custom_protocol.rs index 131f9a7..91dbefa 100644 --- a/examples/async_custom_protocol.rs +++ b/examples/async_custom_protocol.rs @@ -2,102 +2,116 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::path::PathBuf; - -use tao::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, -}; -use wry::{ - http::{header::CONTENT_TYPE, Request, Response}, - WebViewBuilder, -}; - fn main() -> wry::Result<()> { - let event_loop = EventLoop::new(); - let window = WindowBuilder::new().build(&event_loop).unwrap(); + imp::main() +} - let builder = WebViewBuilder::new() - .with_asynchronous_custom_protocol("wry".into(), move |_webview_id, request, responder| { - match get_wry_response(request) { - Ok(http_response) => responder.respond(http_response), - Err(e) => responder.respond( - http::Response::builder() - .header(CONTENT_TYPE, "text/plain") - .status(500) - .body(e.to_string().as_bytes().to_vec()) - .unwrap(), - ), +#[cfg(not(feature = "protocol"))] +mod imp { + pub fn main() -> wry::Result<()> { + unimplemented!() + } +} + +#[cfg(feature = "protocol")] +mod imp { + use std::path::PathBuf; + + use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }; + use wry::{ + http::{header::CONTENT_TYPE, Request, Response}, + WebViewBuilder, + }; + + pub fn main() -> wry::Result<()> { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + let builder = WebViewBuilder::new() + .with_asynchronous_custom_protocol("wry".into(), move |_webview_id, request, responder| { + match get_wry_response(request) { + Ok(http_response) => responder.respond(http_response), + Err(e) => responder.respond( + http::Response::builder() + .header(CONTENT_TYPE, "text/plain") + .status(500) + .body(e.to_string().as_bytes().to_vec()) + .unwrap(), + ), + } + }) + // tell the webview to load the custom protocol + .with_url("wry://localhost"); + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let _webview = builder.build(&window)?; + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let _webview = { + use tao::platform::unix::WindowExtUnix; + use wry::WebViewBuilderExtUnix; + let vbox = window.default_vbox().unwrap(); + builder.build_gtk(vbox)? + }; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } = event + { + *control_flow = ControlFlow::Exit } - }) - // tell the webview to load the custom protocol - .with_url("wry://localhost"); + }); + } - #[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - ))] - let _webview = builder.build(&window)?; - #[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - )))] - let _webview = { - use tao::platform::unix::WindowExtUnix; - use wry::WebViewBuilderExtUnix; - let vbox = window.default_vbox().unwrap(); - builder.build_gtk(vbox)? - }; + fn get_wry_response( + request: Request>, + ) -> Result>, Box> { + let path = request.uri().path(); + // Read the file content from file path + let root = PathBuf::from("examples/custom_protocol"); + let path = if path == "/" { + "index.html" + } else { + // removing leading slash + &path[1..] + }; + let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; + // Return asset contents and mime types based on file extentions + // If you don't want to do this manually, there are some crates for you. + // Such as `infer` and `mime_guess`. + let mimetype = if path.ends_with(".html") || path == "/" { + "text/html" + } else if path.ends_with(".js") { + "text/javascript" + } else if path.ends_with(".png") { + "image/png" + } else if path.ends_with(".wasm") { + "application/wasm" + } else { + unimplemented!(); + }; - if let Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } = event - { - *control_flow = ControlFlow::Exit - } - }); -} - -fn get_wry_response( - request: Request>, -) -> Result>, Box> { - let path = request.uri().path(); - // Read the file content from file path - let root = PathBuf::from("examples/custom_protocol"); - let path = if path == "/" { - "index.html" - } else { - // removing leading slash - &path[1..] - }; - let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; - - // Return asset contents and mime types based on file extentions - // If you don't want to do this manually, there are some crates for you. - // Such as `infer` and `mime_guess`. - let mimetype = if path.ends_with(".html") || path == "/" { - "text/html" - } else if path.ends_with(".js") { - "text/javascript" - } else if path.ends_with(".png") { - "image/png" - } else if path.ends_with(".wasm") { - "application/wasm" - } else { - unimplemented!(); - }; - - Response::builder() - .header(CONTENT_TYPE, mimetype) - .body(content) - .map_err(Into::into) + Response::builder() + .header(CONTENT_TYPE, mimetype) + .body(content) + .map_err(Into::into) + } } diff --git a/examples/custom_protocol.rs b/examples/custom_protocol.rs index b31fbcf..ccf1ae6 100644 --- a/examples/custom_protocol.rs +++ b/examples/custom_protocol.rs @@ -2,102 +2,116 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::path::PathBuf; - -use tao::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, -}; -use wry::{ - http::{header::CONTENT_TYPE, Request, Response}, - WebViewBuilder, -}; - fn main() -> wry::Result<()> { - let event_loop = EventLoop::new(); - let window = WindowBuilder::new().build(&event_loop).unwrap(); - - let builder = WebViewBuilder::new() - .with_custom_protocol( - "wry".into(), - move |_webview_id, request| match get_wry_response(request) { - Ok(r) => r.map(Into::into), - Err(e) => http::Response::builder() - .header(CONTENT_TYPE, "text/plain") - .status(500) - .body(e.to_string().as_bytes().to_vec()) - .unwrap() - .map(Into::into), - }, - ) - // tell the webview to load the custom protocol - .with_url("wry://localhost"); - - #[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - ))] - let _webview = builder.build(&window)?; - #[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - )))] - let _webview = { - use tao::platform::unix::WindowExtUnix; - use wry::WebViewBuilderExtUnix; - let vbox = window.default_vbox().unwrap(); - builder.build_gtk(vbox)? - }; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - if let Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } = event - { - *control_flow = ControlFlow::Exit - } - }); + imp::main() } -fn get_wry_response( - request: Request>, -) -> Result>, Box> { - let path = request.uri().path(); - // Read the file content from file path - let root = PathBuf::from("examples/custom_protocol"); - let path = if path == "/" { - "index.html" - } else { - // removing leading slash - &path[1..] - }; - let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; - - // Return asset contents and mime types based on file extentions - // If you don't want to do this manually, there are some crates for you. - // Such as `infer` and `mime_guess`. - let mimetype = if path.ends_with(".html") || path == "/" { - "text/html" - } else if path.ends_with(".js") { - "text/javascript" - } else if path.ends_with(".png") { - "image/png" - } else if path.ends_with(".wasm") { - "application/wasm" - } else { - unimplemented!(); - }; - - Response::builder() - .header(CONTENT_TYPE, mimetype) - .body(content) - .map_err(Into::into) +#[cfg(not(feature = "protocol"))] +mod imp { + pub fn main() -> wry::Result<()> { + unimplemented!() + } +} + +#[cfg(feature = "protocol")] +mod imp { + use std::path::PathBuf; + + use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }; + use wry::{ + http::{header::CONTENT_TYPE, Request, Response}, + WebViewBuilder, + }; + + pub fn main() -> wry::Result<()> { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + let builder = WebViewBuilder::new() + .with_custom_protocol( + "wry".into(), + move |_webview_id, request| match get_wry_response(request) { + Ok(r) => r.map(Into::into), + Err(e) => http::Response::builder() + .header(CONTENT_TYPE, "text/plain") + .status(500) + .body(e.to_string().as_bytes().to_vec()) + .unwrap() + .map(Into::into), + }, + ) + // tell the webview to load the custom protocol + .with_url("wry://localhost"); + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let _webview = builder.build(&window)?; + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let _webview = { + use tao::platform::unix::WindowExtUnix; + use wry::WebViewBuilderExtUnix; + let vbox = window.default_vbox().unwrap(); + builder.build_gtk(vbox)? + }; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } = event + { + *control_flow = ControlFlow::Exit + } + }); + } + + fn get_wry_response( + request: Request>, + ) -> Result>, Box> { + let path = request.uri().path(); + // Read the file content from file path + let root = PathBuf::from("examples/custom_protocol"); + let path = if path == "/" { + "index.html" + } else { + // removing leading slash + &path[1..] + }; + let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; + + // Return asset contents and mime types based on file extentions + // If you don't want to do this manually, there are some crates for you. + // Such as `infer` and `mime_guess`. + let mimetype = if path.ends_with(".html") || path == "/" { + "text/html" + } else if path.ends_with(".js") { + "text/javascript" + } else if path.ends_with(".png") { + "image/png" + } else if path.ends_with(".wasm") { + "application/wasm" + } else { + unimplemented!(); + }; + + Response::builder() + .header(CONTENT_TYPE, mimetype) + .body(content) + .map_err(Into::into) + } } diff --git a/examples/simple.rs b/examples/simple.rs index 3c5ea08..6b36573 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -18,23 +18,25 @@ fn main() -> wry::Result<()> { .with_new_window_req_handler(|url, features| { println!("new window req: {url} {features:?}"); wry::NewWindowResponse::Allow - }) - .with_drag_drop_handler(|e| { - match e { - wry::DragDropEvent::Enter { paths, position } => { - println!("DragEnter: {position:?} {paths:?} ") - } - wry::DragDropEvent::Over { position } => println!("DragOver: {position:?} "), - wry::DragDropEvent::Drop { paths, position } => { - println!("DragDrop: {position:?} {paths:?} ") - } - wry::DragDropEvent::Leave => println!("DragLeave"), - _ => {} - } - - true }); + #[cfg(feature = "drag-drop")] + let builder = builder.with_drag_drop_handler(|e| { + match e { + wry::DragDropEvent::Enter { paths, position } => { + println!("DragEnter: {position:?} {paths:?} ") + } + wry::DragDropEvent::Over { position } => println!("DragOver: {position:?} "), + wry::DragDropEvent::Drop { paths, position } => { + println!("DragDrop: {position:?} {paths:?} ") + } + wry::DragDropEvent::Leave => println!("DragLeave"), + _ => {} + } + + true + }); + #[cfg(any( target_os = "windows", target_os = "macos", diff --git a/examples/streaming.rs b/examples/streaming.rs index 1b5f508..f7e6e5a 100644 --- a/examples/streaming.rs +++ b/examples/streaming.rs @@ -2,261 +2,276 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{ - io::{Read, Seek, SeekFrom, Write}, - path::PathBuf, -}; - -use http::{header, StatusCode}; -use http_range::HttpRange; -use tao::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, -}; -use wry::{ - http::{header::*, Request, Response}, - WebViewBuilder, -}; - fn main() -> wry::Result<()> { - let event_loop = EventLoop::new(); - let window = WindowBuilder::new().build(&event_loop).unwrap(); - - let builder = WebViewBuilder::new() - .with_custom_protocol( - "wry".into(), - move |_webview_id, request| match wry_protocol(request) { - Ok(r) => r.map(Into::into), - Err(e) => http::Response::builder() - .header(CONTENT_TYPE, "text/plain") - .status(500) - .body(e.to_string().as_bytes().to_vec()) - .unwrap() - .map(Into::into), - }, - ) - .with_custom_protocol( - "stream".into(), - move |_webview_id, request| match stream_protocol(request) { - Ok(r) => r.map(Into::into), - Err(e) => http::Response::builder() - .header(CONTENT_TYPE, "text/plain") - .status(500) - .body(e.to_string().as_bytes().to_vec()) - .unwrap() - .map(Into::into), - }, - ) - // tell the webview to load the custom protocol - .with_url("wry://localhost"); - - #[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - ))] - let _webview = builder.build(&window)?; - #[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - )))] - let _webview = { - use tao::platform::unix::WindowExtUnix; - use wry::WebViewBuilderExtUnix; - let vbox = window.default_vbox().unwrap(); - builder.build_gtk(vbox)? - }; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - if let Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } = event - { - *control_flow = ControlFlow::Exit - } - }); + imp::main() } -fn wry_protocol( - request: Request>, -) -> Result>, Box> { - let path = request.uri().path(); - // Read the file content from file path - let root = PathBuf::from("examples/streaming"); - let path = if path == "/" { - "index.html" - } else { - // removing leading slash - &path[1..] - }; - let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; - - // Return asset contents and mime types based on file extentions - // If you don't want to do this manually, there are some crates for you. - // Such as `infer` and `mime_guess`. - let mimetype = if path.ends_with(".html") || path == "/" { - "text/html" - } else if path.ends_with(".js") { - "text/javascript" - } else { - unimplemented!(); - }; - - Response::builder() - .header(CONTENT_TYPE, mimetype) - .body(content) - .map_err(Into::into) +#[cfg(not(feature = "protocol"))] +mod imp { + pub fn main() -> wry::Result<()> { + unimplemented!() + } } -fn stream_protocol( - request: http::Request>, -) -> Result>, Box> { - // skip leading `/` - let path = percent_encoding::percent_decode(&request.uri().path().as_bytes()[1..]) - .decode_utf8_lossy() - .to_string(); +#[cfg(feature = "protocol")] +mod imp { - let mut file = std::fs::File::open(path)?; - - // get file length - let len = { - let old_pos = file.stream_position()?; - let len = file.seek(SeekFrom::End(0))?; - file.seek(SeekFrom::Start(old_pos))?; - len + use std::{ + io::{Read, Seek, SeekFrom, Write}, + path::PathBuf, }; - let mut resp = Response::builder().header(CONTENT_TYPE, "video/mp4"); + use http::{header, StatusCode}; + use http_range::HttpRange; + use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }; + use wry::{ + http::{header::*, Request, Response}, + WebViewBuilder, + }; - // if the webview sent a range header, we need to send a 206 in return - // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers. - let http_response = if let Some(range_header) = request.headers().get("range") { - let not_satisfiable = || { - Response::builder() - .status(StatusCode::RANGE_NOT_SATISFIABLE) - .header(header::CONTENT_RANGE, format!("bytes */{len}")) - .body(vec![]) + pub fn main() -> wry::Result<()> { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + let builder = WebViewBuilder::new() + .with_custom_protocol( + "wry".into(), + move |_webview_id, request| match wry_protocol(request) { + Ok(r) => r.map(Into::into), + Err(e) => http::Response::builder() + .header(CONTENT_TYPE, "text/plain") + .status(500) + .body(e.to_string().as_bytes().to_vec()) + .unwrap() + .map(Into::into), + }, + ) + .with_custom_protocol( + "stream".into(), + move |_webview_id, request| match stream_protocol(request) { + Ok(r) => r.map(Into::into), + Err(e) => http::Response::builder() + .header(CONTENT_TYPE, "text/plain") + .status(500) + .body(e.to_string().as_bytes().to_vec()) + .unwrap() + .map(Into::into), + }, + ) + // tell the webview to load the custom protocol + .with_url("wry://localhost"); + + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let _webview = builder.build(&window)?; + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let _webview = { + use tao::platform::unix::WindowExtUnix; + use wry::WebViewBuilderExtUnix; + let vbox = window.default_vbox().unwrap(); + builder.build_gtk(vbox)? }; - // parse range header - let ranges = if let Ok(ranges) = HttpRange::parse(range_header.to_str()?, len) { - ranges - .iter() - // map the output back to spec range , example: 0-499 - .map(|r| (r.start, r.start + r.length - 1)) - .collect::>() - } else { - return Ok(not_satisfiable()?); - }; + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; - /// The Maximum bytes we send in one range - const MAX_LEN: u64 = 1000 * 1024; - - if ranges.len() == 1 { - let &(start, mut end) = ranges.first().unwrap(); - - // check if a range is not satisfiable - // - // this should be already taken care of by HttpRange::parse - // but checking here again for extra assurance - if start >= len || end >= len || end < start { - return Ok(not_satisfiable()?); + if let Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } = event + { + *control_flow = ControlFlow::Exit } + }); + } - // adjust end byte for MAX_LEN - end = start + (end - start).min(len - start).min(MAX_LEN - 1); - - // calculate number of bytes needed to be read - let bytes_to_read = end + 1 - start; - - // allocate a buf with a suitable capacity - let mut buf = Vec::with_capacity(bytes_to_read as usize); - // seek the file to the starting byte - file.seek(SeekFrom::Start(start))?; - // read the needed bytes - file.take(bytes_to_read).read_to_end(&mut buf)?; - - resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); - resp = resp.header(CONTENT_LENGTH, end + 1 - start); - resp = resp.status(StatusCode::PARTIAL_CONTENT); - resp.body(buf) + fn wry_protocol( + request: Request>, + ) -> Result>, Box> { + let path = request.uri().path(); + // Read the file content from file path + let root = PathBuf::from("examples/streaming"); + let path = if path == "/" { + "index.html" } else { - let mut buf = Vec::new(); - let ranges = ranges - .iter() - .filter_map(|&(start, mut end)| { - // filter out unsatisfiable ranges - // - // this should be already taken care of by HttpRange::parse - // but checking here again for extra assurance - if start >= len || end >= len || end < start { - None - } else { - // adjust end byte for MAX_LEN - end = start + (end - start).min(len - start).min(MAX_LEN - 1); - Some((start, end)) - } - }) - .collect::>(); + // removing leading slash + &path[1..] + }; + let content = std::fs::read(std::fs::canonicalize(root.join(path))?)?; - let boundary = random_boundary(); - let boundary_sep = format!("\r\n--{boundary}\r\n"); - let boundary_closer = format!("\r\n--{boundary}\r\n"); + // Return asset contents and mime types based on file extentions + // If you don't want to do this manually, there are some crates for you. + // Such as `infer` and `mime_guess`. + let mimetype = if path.ends_with(".html") || path == "/" { + "text/html" + } else if path.ends_with(".js") { + "text/javascript" + } else { + unimplemented!(); + }; - resp = resp.header( - CONTENT_TYPE, - format!("multipart/byteranges; boundary={boundary}"), - ); + Response::builder() + .header(CONTENT_TYPE, mimetype) + .body(content) + .map_err(Into::into) + } - for (end, start) in ranges { - // a new range is being written, write the range boundary - buf.write_all(boundary_sep.as_bytes())?; + fn stream_protocol( + request: http::Request>, + ) -> Result>, Box> { + // skip leading `/` + let path = percent_encoding::percent_decode(&request.uri().path().as_bytes()[1..]) + .decode_utf8_lossy() + .to_string(); - // write the needed headers `Content-Type` and `Content-Range` - buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())?; - buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())?; + let mut file = std::fs::File::open(path)?; - // write the separator to indicate the start of the range body - buf.write_all("\r\n".as_bytes())?; + // get file length + let len = { + let old_pos = file.stream_position()?; + let len = file.seek(SeekFrom::End(0))?; + file.seek(SeekFrom::Start(old_pos))?; + len + }; + + let mut resp = Response::builder().header(CONTENT_TYPE, "video/mp4"); + + // if the webview sent a range header, we need to send a 206 in return + // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers. + let http_response = if let Some(range_header) = request.headers().get("range") { + let not_satisfiable = || { + Response::builder() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(header::CONTENT_RANGE, format!("bytes */{len}")) + .body(vec![]) + }; + + // parse range header + let ranges = if let Ok(ranges) = HttpRange::parse(range_header.to_str()?, len) { + ranges + .iter() + // map the output back to spec range , example: 0-499 + .map(|r| (r.start, r.start + r.length - 1)) + .collect::>() + } else { + return Ok(not_satisfiable()?); + }; + + /// The Maximum bytes we send in one range + const MAX_LEN: u64 = 1000 * 1024; + + if ranges.len() == 1 { + let &(start, mut end) = ranges.first().unwrap(); + + // check if a range is not satisfiable + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + return Ok(not_satisfiable()?); + } + + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); // calculate number of bytes needed to be read let bytes_to_read = end + 1 - start; - let mut local_buf = vec![0_u8; bytes_to_read as usize]; + // allocate a buf with a suitable capacity + let mut buf = Vec::with_capacity(bytes_to_read as usize); + // seek the file to the starting byte file.seek(SeekFrom::Start(start))?; - file.read_exact(&mut local_buf)?; - buf.extend_from_slice(&local_buf); + // read the needed bytes + file.take(bytes_to_read).read_to_end(&mut buf)?; + + resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); + resp = resp.header(CONTENT_LENGTH, end + 1 - start); + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf) + } else { + let mut buf = Vec::new(); + let ranges = ranges + .iter() + .filter_map(|&(start, mut end)| { + // filter out unsatisfiable ranges + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + None + } else { + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + Some((start, end)) + } + }) + .collect::>(); + + let boundary = random_boundary(); + let boundary_sep = format!("\r\n--{boundary}\r\n"); + let boundary_closer = format!("\r\n--{boundary}\r\n"); + + resp = resp.header( + CONTENT_TYPE, + format!("multipart/byteranges; boundary={boundary}"), + ); + + for (end, start) in ranges { + // a new range is being written, write the range boundary + buf.write_all(boundary_sep.as_bytes())?; + + // write the needed headers `Content-Type` and `Content-Range` + buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())?; + buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())?; + + // write the separator to indicate the start of the range body + buf.write_all("\r\n".as_bytes())?; + + // calculate number of bytes needed to be read + let bytes_to_read = end + 1 - start; + + let mut local_buf = vec![0_u8; bytes_to_read as usize]; + file.seek(SeekFrom::Start(start))?; + file.read_exact(&mut local_buf)?; + buf.extend_from_slice(&local_buf); + } + // all ranges have been written, write the closing boundary + buf.write_all(boundary_closer.as_bytes())?; + + resp.body(buf) } - // all ranges have been written, write the closing boundary - buf.write_all(boundary_closer.as_bytes())?; - + } else { + resp = resp.header(CONTENT_LENGTH, len); + let mut buf = Vec::with_capacity(len as usize); + file.read_to_end(&mut buf)?; resp.body(buf) - } - } else { - resp = resp.header(CONTENT_LENGTH, len); - let mut buf = Vec::with_capacity(len as usize); - file.read_to_end(&mut buf)?; - resp.body(buf) - }; + }; - http_response.map_err(Into::into) -} + http_response.map_err(Into::into) + } -fn random_boundary() -> String { - let mut x = [0_u8; 30]; - getrandom::fill(&mut x).expect("failed to get random bytes"); - (x[..]) - .iter() - .map(|&x| format!("{x:x}")) - .fold(String::new(), |mut a, x| { - a.push_str(x.as_str()); - a - }) + fn random_boundary() -> String { + let mut x = [0_u8; 30]; + getrandom::fill(&mut x).expect("failed to get random bytes"); + (x[..]) + .iter() + .map(|&x| format!("{x:x}")) + .fold(String::new(), |mut a, x| { + a.push_str(x.as_str()); + a + }) + } } diff --git a/src/lib.rs b/src/lib.rs index bd1e517..3d5189d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2178,6 +2178,8 @@ pub enum DragDropEvent { } /// Get WebView/Webkit version on current platform. +#[cfg(feature = "os-webview")] +#[cfg_attr(docsrs, doc(cfg(feature = "os-webview")))] pub fn webview_version() -> Result { platform_webview_version() } diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index 87c32c6..479fa40 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -980,7 +980,7 @@ impl InnerWebView { ); if let Some(dt) = cookie.expires_datetime() { - soup_cookie.set_expires(&glib::DateTime::from_unix_utc(dt.unix_timestamp()).unwrap()); + soup_cookie.set_expires(>k::glib::DateTime::from_unix_utc(dt.unix_timestamp()).unwrap()); } if let Some(http_only) = cookie.http_only() {