diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d57aa295..7d8a83aa 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -822,6 +822,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "charset" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" +dependencies = [ + "base64 0.22.1", + "encoding_rs", +] + [[package]] name = "chrono" version = "0.4.41" @@ -7826,6 +7836,7 @@ dependencies = [ name = "yaak-app" version = "0.0.0" dependencies = [ + "charset", "chrono", "cookie", "encoding_rs", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c052d739..4dff8a28 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -83,6 +83,7 @@ yaak-sse = { workspace = true } yaak-sync = { workspace = true } yaak-templates = { workspace = true } yaak-ws = { path = "yaak-ws" } +charset = "0.1.5" [workspace.dependencies] chrono = "0.4.41" diff --git a/src-tauri/src/encoding.rs b/src-tauri/src/encoding.rs index e5aec36a..829b71e4 100644 --- a/src-tauri/src/encoding.rs +++ b/src-tauri/src/encoding.rs @@ -1,16 +1,27 @@ -use encoding_rs::SHIFT_JIS; +use log::debug; +use mime_guess::{Mime, mime}; +use std::path::Path; +use std::str::FromStr; use tokio::fs; -use yaak_models::models::HttpResponse; -pub async fn read_response_body<'a>( - response: HttpResponse, -) -> Option { - let body_path = match response.body_path { - None => return None, - Some(p) => p, - }; +pub async fn read_response_body(body_path: impl AsRef, content_type: &str) -> Option { + let body = fs::read(body_path).await.ok()?; + let body_charset = parse_charset(content_type).unwrap_or("utf-8".to_string()); + debug!("body_charset: {}", body_charset); + if let Some(decoder) = charset::Charset::for_label(body_charset.as_bytes()) { + debug!("Using decoder for charset: {}", body_charset); + let (cow, real_encoding, exist_replace) = decoder.decode(&body); + debug!( + "Decoded body with charset: {}, real_encoding: {:?}, exist_replace: {}", + body_charset, real_encoding, exist_replace + ); + return cow.into_owned().into(); + } - let body = fs::read(body_path).await.unwrap(); - let (s, _, _) = SHIFT_JIS.decode(body.as_slice()); - Some(s.to_string()) + Some(String::from_utf8_lossy(&body).to_string()) +} + +fn parse_charset(content_type: &str) -> Option { + let mime: Mime = Mime::from_str(content_type).ok()?; + mime.get_param(mime::CHARSET).map(|v| v.to_string()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a31b0776..238cd269 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -726,33 +726,43 @@ async fn cmd_format_json(text: &str) -> YaakResult { } #[tauri::command] -async fn cmd_filter_response( +async fn cmd_http_response_body( window: WebviewWindow, app_handle: AppHandle, - response_id: &str, plugin_manager: State<'_, PluginManager>, - filter: &str, + response_id: &str, + filter: Option<&str>, ) -> YaakResult { let response = app_handle.db().get_http_response(response_id)?; - if let None = response.body_path { - return Err(GenericError("Response body path not set".to_string())); - } - - let mut content_type = "".to_string(); - for header in response.headers.iter() { - if header.name.to_lowercase() == "content-type" { - content_type = header.value.to_string().to_lowercase(); - break; + let body_path = match response.body_path { + None => { + return Err(GenericError("Response body path not set".to_string())); } - } + Some(p) => p, + }; - let body = read_response_body(response) + let content_type = response + .headers + .iter() + .find_map(|h| { + if h.name.eq_ignore_ascii_case("content-type") { Some(h.value.as_str()) } else { None } + }) + .unwrap_or_default(); + + let body = read_response_body(&body_path, content_type) .await .ok_or(GenericError("Failed to find response body".to_string()))?; - // TODO: Have plugins register their own content type (regex?) - Ok(plugin_manager.filter_data(&window, filter, &body, &content_type).await?) + match filter { + Some(filter) if !filter.is_empty() => { + Ok(plugin_manager.filter_data(&window, filter, &body, content_type).await?) + } + _ => Ok(FilterResponse { + content: body, + error: None, + }), + } } #[tauri::command] @@ -1330,7 +1340,7 @@ pub fn run() { cmd_delete_send_history, cmd_dismiss_notification, cmd_export_data, - cmd_filter_response, + cmd_http_response_body, cmd_format_json, cmd_get_http_authentication_summaries, cmd_get_http_authentication_config, diff --git a/src-web/components/responseViewers/HTMLOrTextViewer.tsx b/src-web/components/responseViewers/HTMLOrTextViewer.tsx index 243e6c7e..6b4768aa 100644 --- a/src-web/components/responseViewers/HTMLOrTextViewer.tsx +++ b/src-web/components/responseViewers/HTMLOrTextViewer.tsx @@ -13,7 +13,7 @@ interface Props { } export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) { - const rawTextBody = useResponseBodyText(response); + const rawTextBody = useResponseBodyText({ responseId: response.id, filter: null }); const contentType = getContentTypeFromHeaders(response.headers); const language = languageFromContentType(contentType, rawTextBody.data ?? ''); @@ -24,7 +24,7 @@ export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Prop if (language === 'html' && pretty) { return ; } else if (rawTextBody.data == null) { - return Empty response + return Empty response; } else { return ( { if (isSearching) { @@ -58,7 +58,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
{ - if (filter === '') { - return null; - } - - const result = (await invokeCmd('cmd_filter_response', { - responseId, - filter, - })) as FilterResponse; - - if (result.error) { - console.log("Failed to filter response:", result.error); - } - - return result; - }, - }); -} diff --git a/src-web/hooks/useResponseBodyBlob.ts b/src-web/hooks/useResponseBodyBlob.ts deleted file mode 100644 index 85e425f5..00000000 --- a/src-web/hooks/useResponseBodyBlob.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import type { HttpResponse } from '@yaakapp-internal/models'; -import { getResponseBodyBlob } from '../lib/responseBody'; - -export function useResponseBodyBlob(response: HttpResponse) { - return useQuery({ - enabled: response != null, - queryKey: ['response-body-binary', response?.updatedAt], - initialData: null, - queryFn: () => getResponseBodyBlob(response), - }).data; -} diff --git a/src-web/hooks/useResponseBodyText.ts b/src-web/hooks/useResponseBodyText.ts index c73ef412..58db5b59 100644 --- a/src-web/hooks/useResponseBodyText.ts +++ b/src-web/hooks/useResponseBodyText.ts @@ -1,11 +1,15 @@ import { useQuery } from '@tanstack/react-query'; -import type { HttpResponse } from '@yaakapp-internal/models'; import { getResponseBodyText } from '../lib/responseBody'; -export function useResponseBodyText(response: HttpResponse) { +export function useResponseBodyText({ + responseId, + filter, +}: { + responseId: string; + filter: string | null; +}) { return useQuery({ - placeholderData: (prev) => prev, // Keep previous data on refetch - queryKey: ['response-body-text', response.id, response.updatedAt, response.contentLength], - queryFn: () => getResponseBodyText(response), + queryKey: ['response_body_text', responseId, filter ?? ''], + queryFn: () => getResponseBodyText({ responseId, filter }), }); } diff --git a/src-web/lib/responseBody.ts b/src-web/lib/responseBody.ts index ea476007..7efd7826 100644 --- a/src-web/lib/responseBody.ts +++ b/src-web/lib/responseBody.ts @@ -1,23 +1,25 @@ -import { readFile } from '@tauri-apps/plugin-fs'; import type { HttpResponse } from '@yaakapp-internal/models'; +import type { FilterResponse } from '@yaakapp-internal/plugins'; import type { ServerSentEvent } from '@yaakapp-internal/sse'; -import { getCharsetFromContentType } from './model_util'; import { invokeCmd } from './tauri'; -export async function getResponseBodyText(response: HttpResponse): Promise { - if (!response.bodyPath) { - return null; +export async function getResponseBodyText({ + responseId, + filter, +}: { + responseId: string; + filter: string | null; +}): Promise { + const result = await invokeCmd('cmd_http_response_body', { + responseId, + filter, + }); + + if (result.error) { + throw new Error(result.error); } - const bytes = await readFile(response.bodyPath); - const charset = getCharsetFromContentType(response.headers); - - return new TextDecoder(charset ?? 'utf-8', { fatal: false }).decode(bytes); -} - -export async function getResponseBodyBlob(response: HttpResponse): Promise { - if (!response.bodyPath) return null; - return readFile(response.bodyPath); + return result.content; } export async function getResponseBodyEventSource( diff --git a/src-web/lib/tauri.ts b/src-web/lib/tauri.ts index fd020df9..31b6a11b 100644 --- a/src-web/lib/tauri.ts +++ b/src-web/lib/tauri.ts @@ -2,30 +2,29 @@ import type { InvokeArgs } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core'; type TauriCmd = - | 'cmd_get_themes' - | 'cmd_call_http_authentication_action' | 'cmd_call_grpc_request_action' + | 'cmd_call_http_authentication_action' | 'cmd_call_http_request_action' | 'cmd_check_for_updates' | 'cmd_create_grpc_request' | 'cmd_curl_to_request' | 'cmd_decrypt_template' - | 'cmd_secure_template' | 'cmd_delete_all_grpc_connections' | 'cmd_delete_all_http_responses' | 'cmd_delete_send_history' | 'cmd_dismiss_notification' | 'cmd_export_data' - | 'cmd_filter_response' | 'cmd_format_json' | 'cmd_get_http_authentication_config' | 'cmd_get_http_authentication_summaries' | 'cmd_get_sse_events' + | 'cmd_get_themes' | 'cmd_get_workspace_meta' | 'cmd_grpc_go' | 'cmd_grpc_reflect' | 'cmd_grpc_request_actions' | 'cmd_http_request_actions' + | 'cmd_http_response_body' | 'cmd_import_data' | 'cmd_install_plugin' | 'cmd_metadata' @@ -35,6 +34,7 @@ type TauriCmd = | 'cmd_reload_plugins' | 'cmd_render_template' | 'cmd_save_response' + | 'cmd_secure_template' | 'cmd_send_ephemeral_request' | 'cmd_send_http_request' | 'cmd_show_workspace_key'