收口桌面文件导入错误边界
桌面文件导入原生读取失败返回稳定 host_error 文件导入校验错误保留稳定 invalid_request 语义 补充桌面壳门禁和共享决策记录
This commit is contained in:
@@ -2586,15 +2586,23 @@ const requiredRustHostSnippets = [
|
||||
'file_import_cancelled_response',
|
||||
'file_export_unavailable_response',
|
||||
'file_import_unavailable_response',
|
||||
'file_import_payload_error_response',
|
||||
'log_desktop_file_export_failure',
|
||||
'log_desktop_file_import_failure',
|
||||
'desktop file export failed for {label}',
|
||||
'desktop file import failed for {label}',
|
||||
'ImportFilePayloadError::NativeRead',
|
||||
'ImportFilePayloadError::InvalidRequest',
|
||||
'log_desktop_file_export_failure("path.convert")',
|
||||
'log_desktop_file_import_failure("path.convert")',
|
||||
'log_desktop_file_export_failure("write.text")',
|
||||
'log_desktop_file_export_failure("write.image")',
|
||||
'log_desktop_file_export_failure("write.audio")',
|
||||
'file_import_payload_error_response(request, "read.text", error)',
|
||||
'file_import_payload_error_response(request, "read.document", error)',
|
||||
'file_import_payload_error_response(request, "read.image", error)',
|
||||
'file_import_payload_error_response(request, "read.audio", error)',
|
||||
'log_desktop_file_import_failure(label)',
|
||||
'log_desktop_file_import_failure("read.text.join")',
|
||||
'log_desktop_file_import_failure("read.document.join")',
|
||||
'log_desktop_file_import_failure("read.image.join")',
|
||||
@@ -2603,11 +2611,16 @@ const requiredRustHostSnippets = [
|
||||
'desktop_file_unavailable_responses_are_stable',
|
||||
'desktop_file_failures_are_logged_without_exposing_native_detail',
|
||||
'desktop_file_failures_log_stable_label_only',
|
||||
'desktop_file_import_validation_errors_keep_stable_invalid_request',
|
||||
'desktop_file_import_native_read_errors_use_stable_host_error',
|
||||
'"file export cancelled"',
|
||||
'"file import cancelled"',
|
||||
'"file export unavailable"',
|
||||
'"file import unavailable"',
|
||||
'BASE64_STANDARD.decode',
|
||||
'fs::metadata(&path).map_err(|_error| ImportFilePayloadError::NativeRead)',
|
||||
'fs::read(&path).map_err(|_error| ImportFilePayloadError::NativeRead)',
|
||||
'fs::read_to_string(&path).map_err(|_error| ImportFilePayloadError::NativeRead)',
|
||||
'blocking_pick_file',
|
||||
'import_text_file_payload',
|
||||
'import_image_file_payload',
|
||||
@@ -2750,6 +2763,10 @@ const requiredRustHostSnippets = [
|
||||
for (const blockedDesktopFileLogSnippet of [
|
||||
'desktop file export failed for {label}: {error}',
|
||||
'desktop file import failed for {label}: {error}',
|
||||
'Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error)',
|
||||
'fs::metadata(&path).map_err(|error| error.to_string())',
|
||||
'fs::read(&path).map_err(|error| error.to_string())',
|
||||
'fs::read_to_string(&path).map_err(|error| error.to_string())',
|
||||
'log_desktop_file_export_failure("path.convert", &error.to_string())',
|
||||
'log_desktop_file_import_failure("path.convert", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.text", &error.to_string())',
|
||||
|
||||
@@ -14,6 +14,21 @@ const IMPORT_AUDIO_MAX_BYTES: u64 = 20 * 1024 * 1024;
|
||||
const EXPORT_FILE_NAME_FALLBACK: &str = "genarrative-export.txt";
|
||||
const EXPORT_FILE_NAME_MAX_LENGTH: usize = 120;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ImportFilePayloadError {
|
||||
InvalidRequest(&'static str),
|
||||
NativeRead,
|
||||
}
|
||||
|
||||
impl ImportFilePayloadError {
|
||||
pub(crate) fn invalid_message(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::InvalidRequest(message) => Some(message),
|
||||
Self::NativeRead => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_export_file_name(raw_file_name: &str) -> String {
|
||||
let mut file_name = String::new();
|
||||
let mut last_was_space = false;
|
||||
@@ -140,23 +155,30 @@ fn import_document_mime_type(path: &Path) -> Option<&'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn import_text_file_payload(path: PathBuf) -> Result<Value, String> {
|
||||
pub(crate) fn import_text_file_payload(path: PathBuf) -> Result<Value, ImportFilePayloadError> {
|
||||
if !path.is_file() {
|
||||
return Err("text file is required".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"text file is required",
|
||||
));
|
||||
}
|
||||
|
||||
let mime_type =
|
||||
import_text_mime_type(&path).ok_or_else(|| "text MIME must be allowed".to_string())?;
|
||||
let metadata = fs::metadata(&path).map_err(|error| error.to_string())?;
|
||||
let mime_type = import_text_mime_type(&path).ok_or(ImportFilePayloadError::InvalidRequest(
|
||||
"text MIME must be allowed",
|
||||
))?;
|
||||
let metadata = fs::metadata(&path).map_err(|_error| ImportFilePayloadError::NativeRead)?;
|
||||
let byte_count = metadata.len();
|
||||
if byte_count == 0 || byte_count > IMPORT_TEXT_MAX_BYTES {
|
||||
return Err("text exceeds import size limit".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"text exceeds import size limit",
|
||||
));
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&path).map_err(|error| error.to_string())?;
|
||||
let content = fs::read_to_string(&path).map_err(|_error| ImportFilePayloadError::NativeRead)?;
|
||||
let byte_count = content.len() as u64;
|
||||
if byte_count == 0 || byte_count > IMPORT_TEXT_MAX_BYTES {
|
||||
return Err("text exceeds import size limit".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"text exceeds import size limit",
|
||||
));
|
||||
}
|
||||
let file_name = path
|
||||
.file_name()
|
||||
@@ -173,23 +195,30 @@ pub(crate) fn import_text_file_payload(path: PathBuf) -> Result<Value, String> {
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn import_document_file_payload(path: PathBuf) -> Result<Value, String> {
|
||||
pub(crate) fn import_document_file_payload(path: PathBuf) -> Result<Value, ImportFilePayloadError> {
|
||||
if !path.is_file() {
|
||||
return Err("document file is required".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"document file is required",
|
||||
));
|
||||
}
|
||||
|
||||
let mime_type = import_document_mime_type(&path)
|
||||
.ok_or_else(|| "document MIME must be allowed".to_string())?;
|
||||
let metadata = fs::metadata(&path).map_err(|error| error.to_string())?;
|
||||
let mime_type = import_document_mime_type(&path).ok_or(
|
||||
ImportFilePayloadError::InvalidRequest("document MIME must be allowed"),
|
||||
)?;
|
||||
let metadata = fs::metadata(&path).map_err(|_error| ImportFilePayloadError::NativeRead)?;
|
||||
let byte_count = metadata.len();
|
||||
if byte_count == 0 || byte_count > IMPORT_DOCUMENT_MAX_BYTES {
|
||||
return Err("document exceeds import size limit".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"document exceeds import size limit",
|
||||
));
|
||||
}
|
||||
|
||||
let bytes = fs::read(&path).map_err(|error| error.to_string())?;
|
||||
let bytes = fs::read(&path).map_err(|_error| ImportFilePayloadError::NativeRead)?;
|
||||
let byte_count = bytes.len() as u64;
|
||||
if byte_count == 0 || byte_count > IMPORT_DOCUMENT_MAX_BYTES {
|
||||
return Err("document exceeds import size limit".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"document exceeds import size limit",
|
||||
));
|
||||
}
|
||||
let file_name = path
|
||||
.file_name()
|
||||
@@ -302,19 +331,19 @@ fn detect_audio_mime_type(bytes: &[u8]) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn ensure_image_bytes_match_mime_type(bytes: &[u8], mime_type: &str) -> Result<(), String> {
|
||||
fn ensure_image_bytes_match_mime_type(bytes: &[u8], mime_type: &str) -> Result<(), &'static str> {
|
||||
if detect_image_mime_type(bytes) == Some(mime_type) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("image bytes do not match MIME".to_string())
|
||||
Err("image bytes do not match MIME")
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_audio_bytes_match_mime_type(bytes: &[u8], mime_type: &str) -> Result<(), String> {
|
||||
fn ensure_audio_bytes_match_mime_type(bytes: &[u8], mime_type: &str) -> Result<(), &'static str> {
|
||||
if detect_audio_mime_type(bytes) == Some(mime_type) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("audio bytes do not match MIME".to_string())
|
||||
Err("audio bytes do not match MIME")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,25 +515,33 @@ pub(crate) fn import_image_file_payload(
|
||||
path: PathBuf,
|
||||
action: &'static str,
|
||||
position: Option<(i32, i32)>,
|
||||
) -> Result<Value, String> {
|
||||
) -> Result<Value, ImportFilePayloadError> {
|
||||
if !path.is_file() {
|
||||
return Err("image file is required".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"image file is required",
|
||||
));
|
||||
}
|
||||
|
||||
let mime_type =
|
||||
import_image_mime_type(&path).ok_or_else(|| "image MIME must be allowed".to_string())?;
|
||||
let metadata = fs::metadata(&path).map_err(|error| error.to_string())?;
|
||||
let mime_type = import_image_mime_type(&path).ok_or(ImportFilePayloadError::InvalidRequest(
|
||||
"image MIME must be allowed",
|
||||
))?;
|
||||
let metadata = fs::metadata(&path).map_err(|_error| ImportFilePayloadError::NativeRead)?;
|
||||
let byte_count = metadata.len();
|
||||
if byte_count == 0 || byte_count > IMPORT_IMAGE_MAX_BYTES {
|
||||
return Err("image exceeds import size limit".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"image exceeds import size limit",
|
||||
));
|
||||
}
|
||||
|
||||
let bytes = fs::read(&path).map_err(|error| error.to_string())?;
|
||||
let bytes = fs::read(&path).map_err(|_error| ImportFilePayloadError::NativeRead)?;
|
||||
let byte_count = bytes.len() as u64;
|
||||
if byte_count == 0 || byte_count > IMPORT_IMAGE_MAX_BYTES {
|
||||
return Err("image exceeds import size limit".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"image exceeds import size limit",
|
||||
));
|
||||
}
|
||||
ensure_image_bytes_match_mime_type(&bytes, mime_type)?;
|
||||
ensure_image_bytes_match_mime_type(&bytes, mime_type)
|
||||
.map_err(ImportFilePayloadError::InvalidRequest)?;
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
@@ -528,25 +565,33 @@ pub(crate) fn import_image_file_payload(
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
pub(crate) fn import_audio_file_payload(path: PathBuf) -> Result<Value, String> {
|
||||
pub(crate) fn import_audio_file_payload(path: PathBuf) -> Result<Value, ImportFilePayloadError> {
|
||||
if !path.is_file() {
|
||||
return Err("audio file is required".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"audio file is required",
|
||||
));
|
||||
}
|
||||
|
||||
let mime_type =
|
||||
import_audio_mime_type(&path).ok_or_else(|| "audio MIME must be allowed".to_string())?;
|
||||
let metadata = fs::metadata(&path).map_err(|error| error.to_string())?;
|
||||
let mime_type = import_audio_mime_type(&path).ok_or(ImportFilePayloadError::InvalidRequest(
|
||||
"audio MIME must be allowed",
|
||||
))?;
|
||||
let metadata = fs::metadata(&path).map_err(|_error| ImportFilePayloadError::NativeRead)?;
|
||||
let byte_count = metadata.len();
|
||||
if byte_count == 0 || byte_count > IMPORT_AUDIO_MAX_BYTES {
|
||||
return Err("audio exceeds import size limit".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"audio exceeds import size limit",
|
||||
));
|
||||
}
|
||||
|
||||
let bytes = fs::read(&path).map_err(|error| error.to_string())?;
|
||||
let bytes = fs::read(&path).map_err(|_error| ImportFilePayloadError::NativeRead)?;
|
||||
let byte_count = bytes.len() as u64;
|
||||
if byte_count == 0 || byte_count > IMPORT_AUDIO_MAX_BYTES {
|
||||
return Err("audio exceeds import size limit".to_string());
|
||||
return Err(ImportFilePayloadError::InvalidRequest(
|
||||
"audio exceeds import size limit",
|
||||
));
|
||||
}
|
||||
ensure_audio_bytes_match_mime_type(&bytes, mime_type)?;
|
||||
ensure_audio_bytes_match_mime_type(&bytes, mime_type)
|
||||
.map_err(ImportFilePayloadError::InvalidRequest)?;
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
@@ -716,7 +761,7 @@ mod tests {
|
||||
fs::write(&image_path, "text").expect("write image-like text");
|
||||
assert_eq!(
|
||||
import_text_file_payload(image_path.clone()).unwrap_err(),
|
||||
"text MIME must be allowed"
|
||||
ImportFilePayloadError::InvalidRequest("text MIME must be allowed")
|
||||
);
|
||||
fs::remove_file(image_path).expect("remove image-like text");
|
||||
|
||||
@@ -728,7 +773,7 @@ mod tests {
|
||||
.expect("write large text");
|
||||
assert_eq!(
|
||||
import_text_file_payload(large_path.clone()).unwrap_err(),
|
||||
"text exceeds import size limit"
|
||||
ImportFilePayloadError::InvalidRequest("text exceeds import size limit")
|
||||
);
|
||||
fs::remove_file(large_path).expect("remove large text");
|
||||
}
|
||||
@@ -788,7 +833,7 @@ mod tests {
|
||||
fs::write(&image_path, b"text").expect("write image-like document");
|
||||
assert_eq!(
|
||||
import_document_file_payload(image_path.clone()).unwrap_err(),
|
||||
"document MIME must be allowed"
|
||||
ImportFilePayloadError::InvalidRequest("document MIME must be allowed")
|
||||
);
|
||||
fs::remove_file(image_path).expect("remove image-like document");
|
||||
|
||||
@@ -803,7 +848,7 @@ mod tests {
|
||||
.expect("write large document");
|
||||
assert_eq!(
|
||||
import_document_file_payload(large_path.clone()).unwrap_err(),
|
||||
"document exceeds import size limit"
|
||||
ImportFilePayloadError::InvalidRequest("document exceeds import size limit")
|
||||
);
|
||||
fs::remove_file(large_path).expect("remove large document");
|
||||
}
|
||||
@@ -859,7 +904,7 @@ mod tests {
|
||||
fs::write(&text_path, b"text").expect("write text file");
|
||||
assert_eq!(
|
||||
import_image_file_payload(text_path.clone(), "selected", None).unwrap_err(),
|
||||
"image MIME must be allowed"
|
||||
ImportFilePayloadError::InvalidRequest("image MIME must be allowed")
|
||||
);
|
||||
fs::remove_file(text_path).expect("remove text file");
|
||||
|
||||
@@ -870,7 +915,7 @@ mod tests {
|
||||
fs::write(&disguised_path, b"text").expect("write disguised image");
|
||||
assert_eq!(
|
||||
import_image_file_payload(disguised_path.clone(), "selected", None).unwrap_err(),
|
||||
"image bytes do not match MIME"
|
||||
ImportFilePayloadError::InvalidRequest("image bytes do not match MIME")
|
||||
);
|
||||
fs::remove_file(disguised_path).expect("remove disguised image");
|
||||
|
||||
@@ -885,7 +930,7 @@ mod tests {
|
||||
.expect("write large image");
|
||||
assert_eq!(
|
||||
import_image_file_payload(large_path.clone(), "selected", None).unwrap_err(),
|
||||
"image exceeds import size limit"
|
||||
ImportFilePayloadError::InvalidRequest("image exceeds import size limit")
|
||||
);
|
||||
fs::remove_file(large_path).expect("remove large image");
|
||||
}
|
||||
@@ -924,7 +969,7 @@ mod tests {
|
||||
fs::write(&text_path, b"audio").expect("write text file");
|
||||
assert_eq!(
|
||||
import_audio_file_payload(text_path.clone()).unwrap_err(),
|
||||
"audio MIME must be allowed"
|
||||
ImportFilePayloadError::InvalidRequest("audio MIME must be allowed")
|
||||
);
|
||||
fs::remove_file(text_path).expect("remove text file");
|
||||
|
||||
@@ -935,7 +980,7 @@ mod tests {
|
||||
fs::write(&disguised_path, b"audio").expect("write disguised audio");
|
||||
assert_eq!(
|
||||
import_audio_file_payload(disguised_path.clone()).unwrap_err(),
|
||||
"audio bytes do not match MIME"
|
||||
ImportFilePayloadError::InvalidRequest("audio bytes do not match MIME")
|
||||
);
|
||||
fs::remove_file(disguised_path).expect("remove disguised audio");
|
||||
|
||||
@@ -950,7 +995,7 @@ mod tests {
|
||||
.expect("write large audio");
|
||||
assert_eq!(
|
||||
import_audio_file_payload(large_path.clone()).unwrap_err(),
|
||||
"audio exceeds import size limit"
|
||||
ImportFilePayloadError::InvalidRequest("audio exceeds import size limit")
|
||||
);
|
||||
fs::remove_file(large_path).expect("remove large audio");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::host_bridge::file_payloads::{
|
||||
export_audio_payload, export_image_payload, export_text_payload, import_audio_file_payload,
|
||||
import_document_file_payload, import_image_file_payload, import_text_file_payload,
|
||||
write_export_bytes_file, write_export_text_file,
|
||||
write_export_bytes_file, write_export_text_file, ImportFilePayloadError,
|
||||
};
|
||||
use crate::host_bridge::protocol::{failed, ok, HostBridgeRequest, HostBridgeResponse};
|
||||
use serde_json::json;
|
||||
@@ -16,19 +16,11 @@ fn file_import_cancelled_response(request: &HostBridgeRequest) -> HostBridgeResp
|
||||
}
|
||||
|
||||
fn file_export_unavailable_response(request: &HostBridgeRequest) -> HostBridgeResponse {
|
||||
failed(
|
||||
request.id.clone(),
|
||||
"host_error",
|
||||
"file export unavailable",
|
||||
)
|
||||
failed(request.id.clone(), "host_error", "file export unavailable")
|
||||
}
|
||||
|
||||
fn file_import_unavailable_response(request: &HostBridgeRequest) -> HostBridgeResponse {
|
||||
failed(
|
||||
request.id.clone(),
|
||||
"host_error",
|
||||
"file import unavailable",
|
||||
)
|
||||
failed(request.id.clone(), "host_error", "file import unavailable")
|
||||
}
|
||||
|
||||
fn log_desktop_file_export_failure(label: &str) -> bool {
|
||||
@@ -41,6 +33,20 @@ fn log_desktop_file_import_failure(label: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn file_import_payload_error_response(
|
||||
request: &HostBridgeRequest,
|
||||
label: &str,
|
||||
error: ImportFilePayloadError,
|
||||
) -> HostBridgeResponse {
|
||||
match error.invalid_message() {
|
||||
Some(message) => failed(request.id.clone(), "invalid_request", message),
|
||||
None => {
|
||||
log_desktop_file_import_failure(label);
|
||||
file_import_unavailable_response(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn export_desktop_host_bridge_text_file(
|
||||
app: &tauri::AppHandle,
|
||||
request: &HostBridgeRequest,
|
||||
@@ -112,7 +118,7 @@ pub(crate) async fn import_desktop_host_bridge_text_file(
|
||||
tauri::async_runtime::spawn_blocking(move || import_text_file_payload(path)).await;
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Ok(Err(error)) => file_import_payload_error_response(request, "read.text", error),
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("read.text.join");
|
||||
file_import_unavailable_response(request)
|
||||
@@ -146,7 +152,7 @@ pub(crate) async fn import_desktop_host_bridge_document_file(
|
||||
tauri::async_runtime::spawn_blocking(move || import_document_file_payload(path)).await;
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Ok(Err(error)) => file_import_payload_error_response(request, "read.document", error),
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("read.document.join");
|
||||
file_import_unavailable_response(request)
|
||||
@@ -226,7 +232,7 @@ pub(crate) async fn import_desktop_host_bridge_image_file(
|
||||
.await;
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Ok(Err(error)) => file_import_payload_error_response(request, "read.image", error),
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("read.image.join");
|
||||
file_import_unavailable_response(request)
|
||||
@@ -257,7 +263,7 @@ pub(crate) async fn import_desktop_host_bridge_audio_file(
|
||||
tauri::async_runtime::spawn_blocking(move || import_audio_file_payload(path)).await;
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Ok(Err(error)) => file_import_payload_error_response(request, "read.audio", error),
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("read.audio.join");
|
||||
file_import_unavailable_response(request)
|
||||
@@ -364,4 +370,36 @@ mod tests {
|
||||
assert!(!log_desktop_file_export_failure("write.image.join"));
|
||||
assert!(!log_desktop_file_import_failure("read.audio.join"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_file_import_validation_errors_keep_stable_invalid_request() {
|
||||
let request = request("file.importText");
|
||||
|
||||
let error = file_import_payload_error_response(
|
||||
&request,
|
||||
"read.text",
|
||||
ImportFilePayloadError::InvalidRequest("text MIME must be allowed"),
|
||||
)
|
||||
.error
|
||||
.expect("error");
|
||||
|
||||
assert_eq!(error.code, "invalid_request");
|
||||
assert_eq!(error.message, "text MIME must be allowed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_file_import_native_read_errors_use_stable_host_error() {
|
||||
let request = request("file.importText");
|
||||
|
||||
let error = file_import_payload_error_response(
|
||||
&request,
|
||||
"read.text",
|
||||
ImportFilePayloadError::NativeRead,
|
||||
)
|
||||
.error
|
||||
.expect("error");
|
||||
|
||||
assert_eq!(error.code, "host_error");
|
||||
assert_eq!(error.message, "file import unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3166,6 +3166,7 @@
|
||||
- 背景:Tauri 桌面壳已声明文本 / 文档 / 图片 / 音频导入导出;这些能力会打开系统文件对话框、转换系统路径并在后台线程读写文件。如果系统路径转换、后台读写或任务 join 异常只被折叠成稳定 HostBridge 错误,H5 语义安全,但开发侧无法区分系统对话框路径异常、文件系统失败或后台任务失败。
|
||||
- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/files.rs` 必须在导出路径转换、导出写入、导入路径转换和导入后台读取 join 失败时记录 `desktop file export failed for ...` 或 `desktop file import failed for ...` 日志。HostBridge 对 H5 仍只返回稳定错误语义,不透传本地路径、系统错误或线程细节;用户取消系统文件对话框仍返回 `cancelled`,不记录为异常。
|
||||
- 2026-06-21 调整:桌面文件导入导出失败日志只记录 `desktop file export failed for <stage>` / `desktop file import failed for <stage>` 固定标签,不输出本地路径转换错误、文件读写错误、后台任务 join 错误或其它系统细节;配置检查拒绝 `files.rs` 重新拼接 `: {error}` 或把路径 / 读写 / join 错误传给日志函数。
|
||||
- 2026-06-21 调整:桌面文件导入的 MIME、类型和大小校验错误继续以稳定 `invalid_request` 返回 H5;`fs::metadata`、`fs::read`、`fs::read_to_string` 等原生读取失败统一折叠为 `host_error` / `file import unavailable`,只记录 `read.text`、`read.document`、`read.image`、`read.audio` 固定阶段标签,不把系统 IO 错误字符串作为 HostBridge 错误消息或 stderr 明细输出。
|
||||
- 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::files`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-06-20 桌面壳窗口状态小能力系统异常必须可观测
|
||||
|
||||
Reference in New Issue
Block a user