From a4f28f657188515beaec62c86fdd446b0cab4b89 Mon Sep 17 00:00:00 2001 From: Berrysoft Date: Mon, 13 Mar 2023 22:19:17 +0800 Subject: [PATCH 1/6] Parse range in header. --- bins/Cargo.lock | 37 ++++++++++++ bins/ayaka-gui/src-tauri/Cargo.toml | 2 +- .../ayaka-gui/src-tauri/src/asset_resolver.rs | 59 +++++++++++++++---- 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/bins/Cargo.lock b/bins/Cargo.lock index d1ae8ee7..74fa1f2b 100644 --- a/bins/Cargo.lock +++ b/bins/Cargo.lock @@ -119,6 +119,7 @@ dependencies = [ "bitflags", "bytes", "futures-util", + "headers", "http", "http-body", "hyper", @@ -1496,6 +1497,31 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.3.3" @@ -3071,6 +3097,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.6" diff --git a/bins/ayaka-gui/src-tauri/Cargo.toml b/bins/ayaka-gui/src-tauri/Cargo.toml index 61678863..9362a8cc 100644 --- a/bins/ayaka-gui/src-tauri/Cargo.toml +++ b/bins/ayaka-gui/src-tauri/Cargo.toml @@ -16,7 +16,7 @@ serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.2", features = ["cli", "window-all"] } tauri-plugin-window-state = "0.1" -axum = { version = "0.6", default-features = false, features = ["http1", "tokio"] } +axum = { version = "0.6", default-features = false, features = ["http1", "tokio", "headers"] } tower-http = { version = "0.4", features = ["cors", "trace"] } mime_guess = "2.0" stream-future = "0.3" diff --git a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs index 87ce7f56..8cb27507 100644 --- a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs +++ b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs @@ -1,7 +1,11 @@ use axum::{ body::{Body, Bytes, StreamBody}, - extract::Path, - http::{header::CONTENT_TYPE, Request, StatusCode}, + extract::{Path, TypedHeader}, + headers::Range, + http::{ + header::{CONTENT_RANGE, CONTENT_TYPE}, + Request, StatusCode, + }, response::{IntoResponse, Response}, routing::get, Router, Server, @@ -11,6 +15,7 @@ use std::{ fmt::Display, io::{BorrowedBuf, Read}, net::TcpListener, + ops::Bound, sync::OnceLock, }; use stream_future::try_stream; @@ -92,18 +97,48 @@ fn file_stream(mut file: Box, length: usize) -> std::io: Ok(()) } -async fn fs_resolver(Path(path): Path) -> Result { +async fn fs_resolver( + Path(path): Path, + TypedHeader(range): TypedHeader, +) -> Result { let path = ROOT_PATH.get().expect("cannot get ROOT_PATH").join(path)?; - let file = path.open_file()?; + let mut file = path.open_file()?; let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); - let length = path - .metadata() - .map(|meta| meta.len as usize) - .unwrap_or(BUFFER_LEN); - Ok(( - [(CONTENT_TYPE, mime.to_string())], - StreamBody::new(file_stream(file, length)), - )) + let mime_header = (CONTENT_TYPE, mime.to_string()); + let length = path.metadata()?.len; + let range = range.iter().next(); + if let Some((start, end)) = range { + let start = match start { + Bound::Included(i) => i, + Bound::Excluded(i) => i - 1, + Bound::Unbounded => 0, + }; + let end = match end { + Bound::Included(i) => i + 1, + Bound::Excluded(i) => i, + Bound::Unbounded => length, + }; + let read_length = end - start; + let mut buffer = vec![0; read_length as usize]; + file.read_exact(&mut buffer)?; + Ok(( + [ + mime_header, + ( + CONTENT_RANGE, + format!("bytes {}-{}/{}", start, end - 1, length), + ), + ], + buffer, + )) + } else { + let mut buffer = vec![]; + file.read_to_end(&mut buffer)?; + Ok(( + [mime_header, (CONTENT_RANGE, format!("bytes */{}", length))], + buffer, + )) + } } async fn resolver(app: AppHandle, req: Request) -> impl IntoResponse { From 92080d3c66c1f64d2ce811c7ef77cf0cbc06c587 Mon Sep 17 00:00:00 2001 From: Berrysoft Date: Tue, 14 Mar 2023 00:16:26 +0800 Subject: [PATCH 2/6] Remove chunked body. --- bins/Cargo.lock | 1 - bins/ayaka-gui/src-tauri/Cargo.toml | 1 - .../ayaka-gui/src-tauri/src/asset_resolver.rs | 60 ++++++------------- bins/ayaka-gui/src-tauri/src/main.rs | 2 - 4 files changed, 18 insertions(+), 46 deletions(-) diff --git a/bins/Cargo.lock b/bins/Cargo.lock index 74fa1f2b..80a4cee7 100644 --- a/bins/Cargo.lock +++ b/bins/Cargo.lock @@ -188,7 +188,6 @@ dependencies = [ "mime_guess", "serde", "serde_json", - "stream-future", "tauri", "tauri-build", "tauri-plugin-window-state", diff --git a/bins/ayaka-gui/src-tauri/Cargo.toml b/bins/ayaka-gui/src-tauri/Cargo.toml index 9362a8cc..6295d471 100644 --- a/bins/ayaka-gui/src-tauri/Cargo.toml +++ b/bins/ayaka-gui/src-tauri/Cargo.toml @@ -19,7 +19,6 @@ tauri-plugin-window-state = "0.1" axum = { version = "0.6", default-features = false, features = ["http1", "tokio", "headers"] } tower-http = { version = "0.4", features = ["cors", "trace"] } mime_guess = "2.0" -stream-future = "0.3" [features] default = [ "custom-protocol" ] diff --git a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs index 8cb27507..7e0c568b 100644 --- a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs +++ b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs @@ -1,5 +1,5 @@ use axum::{ - body::{Body, Bytes, StreamBody}, + body::Body, extract::{Path, TypedHeader}, headers::Range, http::{ @@ -13,12 +13,11 @@ use axum::{ use ayaka_model::vfs::{error::VfsErrorKind, *}; use std::{ fmt::Display, - io::{BorrowedBuf, Read}, + io::{Read, SeekFrom}, net::TcpListener, ops::Bound, sync::OnceLock, }; -use stream_future::try_stream; use tauri::{ plugin::{Builder, TauriPlugin}, AppHandle, Runtime, @@ -68,45 +67,16 @@ impl From for ResolverError { } } -const BUFFER_LEN: usize = 1048576; - -fn read_buf_vec(mut file: impl Read, vec: &mut Vec) -> std::io::Result { - let old_len = vec.len(); - let mut read_buf = BorrowedBuf::from(vec.spare_capacity_mut()); - let mut cursor = read_buf.unfilled(); - file.read_buf(cursor.reborrow())?; - let written = cursor.written(); - unsafe { - vec.set_len(old_len + written); - } - Ok(written) -} - -#[try_stream(Bytes)] -fn file_stream(mut file: Box, length: usize) -> std::io::Result<()> { - let length = length.min(BUFFER_LEN); - loop { - let mut buffer = Vec::with_capacity(length); - let read_bytes = read_buf_vec(&mut file, &mut buffer)?; - if read_bytes > 0 { - yield Bytes::from(buffer); - } else { - break; - } - } - Ok(()) -} - async fn fs_resolver( Path(path): Path, - TypedHeader(range): TypedHeader, -) -> Result { + range: Option>, +) -> Result { let path = ROOT_PATH.get().expect("cannot get ROOT_PATH").join(path)?; - let mut file = path.open_file()?; let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); let mime_header = (CONTENT_TYPE, mime.to_string()); let length = path.metadata()?.len; - let range = range.iter().next(); + let mut file = path.open_file()?; + let range = range.and_then(|TypedHeader(range)| range.iter().next()); if let Some((start, end)) = range { let start = match start { Bound::Included(i) => i, @@ -117,11 +87,19 @@ async fn fs_resolver( Bound::Included(i) => i + 1, Bound::Excluded(i) => i, Bound::Unbounded => length, - }; + } + .min(length); let read_length = end - start; let mut buffer = vec![0; read_length as usize]; + file.seek(SeekFrom::Start(start))?; file.read_exact(&mut buffer)?; + let code = if read_length < length { + StatusCode::PARTIAL_CONTENT + } else { + StatusCode::OK + }; Ok(( + code, [ mime_header, ( @@ -130,14 +108,12 @@ async fn fs_resolver( ), ], buffer, - )) + ) + .into_response()) } else { let mut buffer = vec![]; file.read_to_end(&mut buffer)?; - Ok(( - [mime_header, (CONTENT_RANGE, format!("bytes */{}", length))], - buffer, - )) + Ok(([mime_header], buffer).into_response()) } } diff --git a/bins/ayaka-gui/src-tauri/src/main.rs b/bins/ayaka-gui/src-tauri/src/main.rs index 657790df..821e9de1 100644 --- a/bins/ayaka-gui/src-tauri/src/main.rs +++ b/bins/ayaka-gui/src-tauri/src/main.rs @@ -2,9 +2,7 @@ all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] -#![feature(generators)] #![feature(once_cell)] -#![feature(read_buf)] #![feature(return_position_impl_trait_in_trait)] #![allow(incomplete_features)] From 7212996f239bda95aa056bc06f6d5b1a977287ac Mon Sep 17 00:00:00 2001 From: Berrysoft Date: Tue, 14 Mar 2023 00:25:11 +0800 Subject: [PATCH 3/6] Get length only when asking ranges. --- bins/ayaka-gui/src-tauri/src/asset_resolver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs index 7e0c568b..1d1328cb 100644 --- a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs +++ b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs @@ -74,10 +74,10 @@ async fn fs_resolver( let path = ROOT_PATH.get().expect("cannot get ROOT_PATH").join(path)?; let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); let mime_header = (CONTENT_TYPE, mime.to_string()); - let length = path.metadata()?.len; let mut file = path.open_file()?; let range = range.and_then(|TypedHeader(range)| range.iter().next()); if let Some((start, end)) = range { + let length = path.metadata()?.len; let start = match start { Bound::Included(i) => i, Bound::Excluded(i) => i - 1, From 883b1807f5f9f5cda5fd71abd29b21a5186c29c5 Mon Sep 17 00:00:00 2001 From: Berrysoft Date: Tue, 14 Mar 2023 01:26:38 +0800 Subject: [PATCH 4/6] Simplify headers. --- .../ayaka-gui/src-tauri/src/asset_resolver.rs | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs index 1d1328cb..d23650c2 100644 --- a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs +++ b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs @@ -1,11 +1,8 @@ use axum::{ body::Body, extract::{Path, TypedHeader}, - headers::Range, - http::{ - header::{CONTENT_RANGE, CONTENT_TYPE}, - Request, StatusCode, - }, + headers::{ContentRange, ContentType, HeaderMapExt, Range}, + http::{header::CONTENT_TYPE, HeaderMap, Request, StatusCode}, response::{IntoResponse, Response}, routing::get, Router, Server, @@ -67,28 +64,43 @@ impl From for ResolverError { } } +fn get_first_range(range: Range, length: u64) -> Option<(u64, u64)> { + let mut iter = range.iter(); + let (start, end) = iter.next()?; + // We don't support multiple ranges. + if let Some(_) = iter.next() { + return None; + } + let start = match start { + Bound::Included(i) => i, + Bound::Excluded(i) => i - 1, + Bound::Unbounded => 0, + }; + let end = match end { + Bound::Included(i) => i + 1, + Bound::Excluded(i) => i, + Bound::Unbounded => length, + } + .min(length); + Some((start, end)) +} + async fn fs_resolver( Path(path): Path, range: Option>, ) -> Result { let path = ROOT_PATH.get().expect("cannot get ROOT_PATH").join(path)?; let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); - let mime_header = (CONTENT_TYPE, mime.to_string()); + let mut header_map = HeaderMap::new(); + header_map.typed_insert(ContentType::from(mime)); let mut file = path.open_file()?; - let range = range.and_then(|TypedHeader(range)| range.iter().next()); - if let Some((start, end)) = range { + if let Some(TypedHeader(range)) = range { let length = path.metadata()?.len; - let start = match start { - Bound::Included(i) => i, - Bound::Excluded(i) => i - 1, - Bound::Unbounded => 0, + let (start, end) = if let Some(range) = get_first_range(range, length) { + range + } else { + return Ok(StatusCode::RANGE_NOT_SATISFIABLE.into_response()); }; - let end = match end { - Bound::Included(i) => i + 1, - Bound::Excluded(i) => i, - Bound::Unbounded => length, - } - .min(length); let read_length = end - start; let mut buffer = vec![0; read_length as usize]; file.seek(SeekFrom::Start(start))?; @@ -98,22 +110,12 @@ async fn fs_resolver( } else { StatusCode::OK }; - Ok(( - code, - [ - mime_header, - ( - CONTENT_RANGE, - format!("bytes {}-{}/{}", start, end - 1, length), - ), - ], - buffer, - ) - .into_response()) + header_map.typed_insert(ContentRange::bytes(start..end, length).unwrap()); + Ok((code, header_map, buffer).into_response()) } else { let mut buffer = vec![]; file.read_to_end(&mut buffer)?; - Ok(([mime_header], buffer).into_response()) + Ok((header_map, buffer).into_response()) } } From d97e8d09993445d9d02bae37834a562915bb7c71 Mon Sep 17 00:00:00 2001 From: Berrysoft Date: Tue, 14 Mar 2023 22:37:13 +0800 Subject: [PATCH 5/6] Deal with invalid range correctly. --- .../ayaka-gui/src-tauri/src/asset_resolver.rs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs index d23650c2..64ca2a10 100644 --- a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs +++ b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs @@ -64,6 +64,14 @@ impl From for ResolverError { } } +struct RangeNotSatisfiableError; + +impl From for ResolverError { + fn from(_: RangeNotSatisfiableError) -> Self { + Self(StatusCode::RANGE_NOT_SATISFIABLE, String::new()) + } +} + fn get_first_range(range: Range, length: u64) -> Option<(u64, u64)> { let mut iter = range.iter(); let (start, end) = iter.next()?; @@ -80,15 +88,18 @@ fn get_first_range(range: Range, length: u64) -> Option<(u64, u64)> { Bound::Included(i) => i + 1, Bound::Excluded(i) => i, Bound::Unbounded => length, + }; + if end > length { + None + } else { + Some((start, end)) } - .min(length); - Some((start, end)) } async fn fs_resolver( Path(path): Path, range: Option>, -) -> Result { +) -> Result { let path = ROOT_PATH.get().expect("cannot get ROOT_PATH").join(path)?; let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); let mut header_map = HeaderMap::new(); @@ -96,26 +107,17 @@ async fn fs_resolver( let mut file = path.open_file()?; if let Some(TypedHeader(range)) = range { let length = path.metadata()?.len; - let (start, end) = if let Some(range) = get_first_range(range, length) { - range - } else { - return Ok(StatusCode::RANGE_NOT_SATISFIABLE.into_response()); - }; + let (start, end) = get_first_range(range, length).ok_or(RangeNotSatisfiableError)?; let read_length = end - start; let mut buffer = vec![0; read_length as usize]; file.seek(SeekFrom::Start(start))?; file.read_exact(&mut buffer)?; - let code = if read_length < length { - StatusCode::PARTIAL_CONTENT - } else { - StatusCode::OK - }; header_map.typed_insert(ContentRange::bytes(start..end, length).unwrap()); - Ok((code, header_map, buffer).into_response()) + Ok((StatusCode::PARTIAL_CONTENT, header_map, buffer)) } else { let mut buffer = vec![]; file.read_to_end(&mut buffer)?; - Ok((header_map, buffer).into_response()) + Ok((StatusCode::OK, header_map, buffer)) } } From 44188108cf7909096671d4627363ba0d861e6bfe Mon Sep 17 00:00:00 2001 From: Berrysoft Date: Tue, 14 Mar 2023 23:49:44 +0800 Subject: [PATCH 6/6] Use read_buf_exact. --- .../ayaka-gui/src-tauri/src/asset_resolver.rs | 21 ++++++++++++++++--- bins/ayaka-gui/src-tauri/src/main.rs | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs index 64ca2a10..6ca7000c 100644 --- a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs +++ b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs @@ -10,7 +10,7 @@ use axum::{ use ayaka_model::vfs::{error::VfsErrorKind, *}; use std::{ fmt::Display, - io::{Read, SeekFrom}, + io::{BorrowedBuf, Read, SeekFrom}, net::TcpListener, ops::Bound, sync::OnceLock, @@ -96,6 +96,21 @@ fn get_first_range(range: Range, length: u64) -> Option<(u64, u64)> { } } +fn read_buf_exact(mut file: impl Read, buffer: &mut Vec, length: usize) -> std::io::Result<()> { + let old_len = buffer.len(); + buffer.reserve_exact(length); + // SAFETY: reserved + let mut read_buf = + BorrowedBuf::from(unsafe { buffer.spare_capacity_mut().get_unchecked_mut(..length) }); + let cursor = read_buf.unfilled(); + file.read_buf_exact(cursor)?; + // SAFETY: read exact + unsafe { + buffer.set_len(old_len + length); + } + Ok(()) +} + async fn fs_resolver( Path(path): Path, range: Option>, @@ -109,9 +124,9 @@ async fn fs_resolver( let length = path.metadata()?.len; let (start, end) = get_first_range(range, length).ok_or(RangeNotSatisfiableError)?; let read_length = end - start; - let mut buffer = vec![0; read_length as usize]; + let mut buffer = vec![]; file.seek(SeekFrom::Start(start))?; - file.read_exact(&mut buffer)?; + read_buf_exact(file, &mut buffer, read_length as usize)?; header_map.typed_insert(ContentRange::bytes(start..end, length).unwrap()); Ok((StatusCode::PARTIAL_CONTENT, header_map, buffer)) } else { diff --git a/bins/ayaka-gui/src-tauri/src/main.rs b/bins/ayaka-gui/src-tauri/src/main.rs index 821e9de1..3972a165 100644 --- a/bins/ayaka-gui/src-tauri/src/main.rs +++ b/bins/ayaka-gui/src-tauri/src/main.rs @@ -3,6 +3,7 @@ windows_subsystem = "windows" )] #![feature(once_cell)] +#![feature(read_buf)] #![feature(return_position_impl_trait_in_trait)] #![allow(incomplete_features)]