Skip to content

Commit 92ba272

Browse files
authored
[nae][tauri] Add support for more content-encoding methods
1 parent c3761a9 commit 92ba272

File tree

4 files changed

+197
-5
lines changed

4 files changed

+197
-5
lines changed

‎Cargo.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,15 @@ http = "1"
3333
reqwest = { version = "0.12", default-features = false }
3434
once_cell = "1"
3535
tokio = { version = "1", features = ["macros"] }
36+
flate2 = "1.1.2"
37+
brotli = "8.0.2"
3638

3739
[build-dependencies]
3840
tauri-plugin = { version = "2", features = ["build"] }
3941

42+
[dev-dependencies]
43+
httpmock = "0.6"
44+
4045
[features]
4146
default = [
4247
"rustls-tls",

‎src/command_test.rs‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#[cfg(test)]
2+
mod tests {
3+
use crate::commands::{get_response, build_request, RequestConfig};
4+
use url::Url;
5+
use tokio::sync::oneshot;
6+
use flate2::{write::GzEncoder, write::DeflateEncoder, Compression};
7+
use brotli::CompressorWriter;
8+
use std::io::Write;
9+
10+
fn encode_gzip(data: &[u8]) -> Vec<u8> {
11+
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
12+
encoder.write_all(data).unwrap();
13+
encoder.finish().unwrap()
14+
}
15+
16+
fn encode_deflate(data: &[u8]) -> Vec<u8> {
17+
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
18+
encoder.write_all(data).unwrap();
19+
encoder.finish().unwrap()
20+
}
21+
22+
fn encode_brotli(data: &[u8]) -> Vec<u8> {
23+
let mut encoder = CompressorWriter::new(Vec::new(), 4096, 5, 22);
24+
encoder.write_all(data).unwrap();
25+
encoder.into_inner()
26+
}
27+
28+
#[tokio::test]
29+
async fn test_get_response_gzip() {
30+
use httpmock::MockServer;
31+
let server = MockServer::start_async().await;
32+
let data = b"hello gzip";
33+
let encoded = encode_gzip(data);
34+
let mock = server.mock_async(|when, then| {
35+
when.method("GET").path("/test_gzip");
36+
then.status(200)
37+
.header("content-encoding", "gzip")
38+
.body(encoded.clone());
39+
}).await;
40+
let url = Url::parse(&format!("{}test_gzip", server.url("/"))).unwrap();
41+
let request_config = RequestConfig::new(
42+
1,
43+
"GET".to_string(),
44+
url,
45+
vec![],
46+
None,
47+
None,
48+
None,
49+
None,
50+
);
51+
let request = build_request(request_config).unwrap();
52+
let (_tx, rx) = oneshot::channel();
53+
let response = get_response(request, rx).await.unwrap();
54+
assert_eq!(response.body().as_ref().unwrap(), data);
55+
mock.assert_async().await;
56+
}
57+
58+
#[tokio::test]
59+
async fn test_get_response_deflate() {
60+
use httpmock::MockServer;
61+
let server = MockServer::start_async().await;
62+
let data = b"hello deflate";
63+
let encoded = encode_deflate(data);
64+
let mock = server.mock_async(|when, then| {
65+
when.method("GET").path("/test_deflate");
66+
then.status(200)
67+
.header("content-encoding", "deflate")
68+
.body(encoded.clone());
69+
}).await;
70+
let url = Url::parse(&format!("{}test_deflate", server.url("/"))).unwrap();
71+
let request_config = RequestConfig::new(
72+
2,
73+
"GET".to_string(),
74+
url,
75+
vec![],
76+
None,
77+
None,
78+
None,
79+
None,
80+
);
81+
let request = build_request(request_config).unwrap();
82+
let (_tx, rx) = oneshot::channel();
83+
let response = get_response(request, rx).await.unwrap();
84+
assert_eq!(response.body().as_ref().unwrap(), data);
85+
mock.assert_async().await;
86+
}
87+
88+
#[tokio::test]
89+
async fn test_get_response_brotli() {
90+
use httpmock::MockServer;
91+
let server = MockServer::start_async().await;
92+
let data = b"hello brotli";
93+
let encoded = encode_brotli(data);
94+
let mock = server.mock_async(|when, then| {
95+
when.method("GET").path("/test_brotli");
96+
then.status(200)
97+
.header("content-encoding", "br")
98+
.body(encoded.clone());
99+
}).await;
100+
let url = Url::parse(&format!("{}test_brotli", server.url("/"))).unwrap();
101+
let request_config = RequestConfig::new(
102+
3,
103+
"GET".to_string(),
104+
url,
105+
vec![],
106+
None,
107+
None,
108+
None,
109+
None,
110+
);
111+
let request = build_request(request_config).unwrap();
112+
let (_tx, rx) = oneshot::channel();
113+
let response = get_response(request, rx).await.unwrap();
114+
assert_eq!(response.body().as_ref().unwrap(), data);
115+
mock.assert_async().await;
116+
}
117+
}

‎src/commands.rs‎

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@ pub struct RequestConfig {
2626
proxy: Option<Proxy>,
2727
}
2828

29+
#[cfg(test)]
30+
impl RequestConfig {
31+
pub(crate) fn new(
32+
request_id: u64,
33+
method: String,
34+
url: url::Url,
35+
headers: Vec<(String, String)>,
36+
data: Option<Vec<u8>>,
37+
connect_timeout: Option<u64>,
38+
max_redirections: Option<usize>,
39+
proxy: Option<Proxy>,
40+
) -> Self {
41+
Self {
42+
request_id,
43+
method,
44+
url,
45+
headers,
46+
data,
47+
connect_timeout,
48+
max_redirections,
49+
proxy,
50+
}
51+
}
52+
}
53+
2954
#[derive(Serialize)]
3055
#[serde(rename_all = "camelCase")]
3156
pub struct FetchResponse {
@@ -36,6 +61,13 @@ pub struct FetchResponse {
3661
body: Option<Vec<u8>>,
3762
}
3863

64+
#[cfg(test)]
65+
impl FetchResponse {
66+
pub(crate) fn body(&self) -> &Option<Vec<u8>> {
67+
&self.body
68+
}
69+
}
70+
3971
use once_cell::sync::Lazy;
4072
use tokio::sync::oneshot;
4173
type RequestPool = Arc<std::sync::Mutex<HashMap<u64, oneshot::Sender<()>>>>;
@@ -141,18 +173,54 @@ pub async fn get_response(
141173
let status = res.status();
142174
let url = res.url().to_string();
143175
let mut headers = Vec::new();
176+
let mut content_encoding = None;
144177
for (key, val) in res.headers().iter() {
145-
headers.push((
146-
key.as_str().into(),
147-
String::from_utf8(val.as_bytes().to_vec())?,
148-
));
178+
let key_str: String = key.as_str().into();
179+
let val_str = String::from_utf8(val.as_bytes().to_vec())?;
180+
if key_str.eq_ignore_ascii_case("content-encoding") {
181+
content_encoding = Some(val_str.to_lowercase());
182+
}
183+
headers.push((key_str, val_str));
149184
}
185+
let bytes = res.bytes().await?;
186+
let body = if let Some(enc) = content_encoding {
187+
match enc.as_str() {
188+
"gzip" => {
189+
use flate2::read::GzDecoder;
190+
use std::io::Read;
191+
let mut d = GzDecoder::new(&bytes[..]);
192+
let mut decompressed = Vec::new();
193+
d.read_to_end(&mut decompressed)?;
194+
decompressed
195+
},
196+
"deflate" => {
197+
use flate2::read::DeflateDecoder;
198+
use std::io::Read;
199+
let mut d = DeflateDecoder::new(&bytes[..]);
200+
let mut decompressed = Vec::new();
201+
d.read_to_end(&mut decompressed)?;
202+
decompressed
203+
},
204+
"br" => {
205+
use brotli::Decompressor;
206+
use std::io::Read;
207+
let mut d = Decompressor::new(&bytes[..], 4096);
208+
let mut decompressed = Vec::new();
209+
d.read_to_end(&mut decompressed)?;
210+
decompressed
211+
},
212+
"identity" => bytes.to_vec(),
213+
_ => bytes.to_vec(),
214+
}
215+
} else {
216+
bytes.to_vec()
217+
};
150218
return Ok(FetchResponse {
151219
status: status.as_u16(),
152220
status_text: status.canonical_reason().unwrap_or_default().to_string(),
153221
headers,
154222
url,
155-
body: Some(res.bytes().await?.to_vec()),
223+
body: Some(body),
156224
});
157225
}
158226
Err(err) => return Err(Error::Network(err)),

‎src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use tauri::{
1515
pub use error::{Error, Result};
1616
mod commands;
1717
mod error;
18+
#[cfg(test)]
19+
mod command_test;
1820

1921
pub fn init<R: Runtime>() -> TauriPlugin<R> {
2022
Builder::<R>::new("cors-fetch")

0 commit comments

Comments
 (0)