app/rust/src/api/unp4k_api.rs
2025-12-11 00:54:30 +08:00

427 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use anyhow::{anyhow, Result};
use flutter_rust_bridge::frb;
use rayon::prelude::*;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use unp4k::dataforge::DataForge;
use unp4k::{CryXmlReader, P4kEntry, P4kFile};
/// P4K 文件项信息
#[frb(dart_metadata=("freezed"))]
pub struct P4kFileItem {
/// 文件名/路径
pub name: String,
/// 是否为目录
pub is_directory: bool,
/// 文件大小(字节)
pub size: u64,
/// 压缩后大小(字节)
pub compressed_size: u64,
/// 文件修改时间(毫秒时间戳)
pub date_modified: i64,
}
/// 将 DOS 日期时间转换为毫秒时间戳
fn dos_datetime_to_millis(date: u16, time: u16) -> i64 {
let year = ((date >> 9) & 0x7F) as i32 + 1980;
let month = ((date >> 5) & 0x0F) as u32;
let day = (date & 0x1F) as u32;
let hour = ((time >> 11) & 0x1F) as u32;
let minute = ((time >> 5) & 0x3F) as u32;
let second = ((time & 0x1F) * 2) as u32;
let days_since_epoch = {
let mut days = 0i64;
for y in 1970..year {
days += if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
366
} else {
365
};
}
let days_in_months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
if month >= 1 && month <= 12 {
days += days_in_months[(month - 1) as usize] as i64;
if month > 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
days += 1;
}
}
days += (day as i64) - 1;
days
};
(days_since_epoch * 86400 + (hour as i64) * 3600 + (minute as i64) * 60 + (second as i64))
* 1000
}
// 全局 P4K 读取器实例(用于保持状态)
static GLOBAL_P4K_READER: once_cell::sync::Lazy<Arc<Mutex<Option<P4kFile>>>> =
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(None)));
static GLOBAL_P4K_FILES: once_cell::sync::Lazy<Arc<Mutex<HashMap<String, P4kEntry>>>> =
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(HashMap::new())));
// 全局 DataForge 实例(用于 DCB 文件解析)
static GLOBAL_DCB_READER: once_cell::sync::Lazy<Arc<Mutex<Option<DataForge>>>> =
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(None)));
/// 打开 P4K 文件(仅打开,不读取文件列表)
pub async fn p4k_open(p4k_path: String) -> Result<()> {
let path = PathBuf::from(&p4k_path);
if !path.exists() {
return Err(anyhow!("P4K file not found: {}", p4k_path));
}
// 在后台线程执行阻塞操作
let reader = tokio::task::spawn_blocking(move || {
let reader = P4kFile::open(&path)?;
Ok::<_, anyhow::Error>(reader)
})
.await??;
*GLOBAL_P4K_READER.lock().unwrap() = Some(reader);
// 清空之前的文件列表缓存
GLOBAL_P4K_FILES.lock().unwrap().clear();
Ok(())
}
/// 确保文件列表已加载(内部使用)
fn ensure_files_loaded() -> Result<usize> {
let mut files = GLOBAL_P4K_FILES.lock().unwrap();
if !files.is_empty() {
return Ok(files.len());
}
let reader = GLOBAL_P4K_READER.lock().unwrap();
if reader.is_none() {
return Err(anyhow!("P4K reader not initialized"));
}
let entries = reader.as_ref().unwrap().entries();
for entry in entries {
let name = if entry.name.starts_with("\\") {
entry.name.clone()
} else {
format!("\\{}", entry.name.replace("/", "\\"))
};
files.insert(name, entry.clone());
}
Ok(files.len())
}
/// 获取文件数量(会触发文件列表加载)
pub async fn p4k_get_file_count() -> Result<usize> {
tokio::task::spawn_blocking(|| ensure_files_loaded()).await?
}
/// 获取所有文件列表
pub async fn p4k_get_all_files() -> Result<Vec<P4kFileItem>> {
tokio::task::spawn_blocking(|| {
ensure_files_loaded()?;
let files = GLOBAL_P4K_FILES.lock().unwrap();
let mut result = Vec::with_capacity(files.len());
for (name, entry) in files.iter() {
result.push(P4kFileItem {
name: name.clone(),
is_directory: false,
size: entry.uncompressed_size,
compressed_size: entry.compressed_size,
date_modified: dos_datetime_to_millis(entry.mod_date, entry.mod_time),
});
}
Ok(result)
})
.await?
}
/// 提取文件到内存
pub async fn p4k_extract_to_memory(file_path: String) -> Result<Vec<u8>> {
// 确保文件列表已加载
tokio::task::spawn_blocking(|| ensure_files_loaded()).await??;
// 获取文件 entry 的克隆
let entry = p4k_get_entry(file_path).await?;
// 在后台线程执行阻塞的提取操作
let data = tokio::task::spawn_blocking(move || {
let mut reader = GLOBAL_P4K_READER.lock().unwrap();
if reader.is_none() {
return Err(anyhow!("P4K reader not initialized"));
}
let data = reader.as_mut().unwrap().extract_entry(&entry)?;
if (entry.name.ends_with(".xml") || entry.name.ends_with(".mtl"))
&& CryXmlReader::is_cryxml(&data)
{
let cry_xml_string = CryXmlReader::parse(&data)?;
return Ok(cry_xml_string.into_bytes());
}
Ok::<_, anyhow::Error>(data)
})
.await??;
Ok(data)
}
async fn p4k_get_entry(file_path: String) -> Result<P4kEntry> {
// 确保文件列表已加载
tokio::task::spawn_blocking(|| ensure_files_loaded()).await??;
// 规范化路径
let normalized_path = if file_path.starts_with("\\") {
file_path.clone()
} else {
format!("\\{}", file_path)
};
// 获取文件 entry 的克隆
let entry = {
let files = GLOBAL_P4K_FILES.lock().unwrap();
files
.get(&normalized_path)
.ok_or_else(|| anyhow!("File not found: {}", file_path))?
.clone()
};
Ok(entry)
}
/// 提取文件到磁盘
pub async fn p4k_extract_to_disk(file_path: String, output_path: String) -> Result<()> {
let entry = p4k_get_entry(file_path).await?;
// 在后台线程执行阻塞的文件写入操作
tokio::task::spawn_blocking(move || {
let output = PathBuf::from(&output_path);
// 创建父目录
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent)?;
}
let mut reader_guard = GLOBAL_P4K_READER.lock().unwrap();
let reader = reader_guard
.as_mut()
.ok_or_else(|| anyhow!("P4K reader not initialized"))?;
unp4k::p4k_utils::extract_single_file(reader, &entry, &output, true)?;
Ok::<_, anyhow::Error>(())
})
.await??;
Ok(())
}
/// 关闭 P4K 读取器
pub async fn p4k_close() -> Result<()> {
*GLOBAL_P4K_READER.lock().unwrap() = None;
GLOBAL_P4K_FILES.lock().unwrap().clear();
Ok(())
}
// ==================== DataForge/DCB API ====================
/// DCB 记录项信息
#[frb(dart_metadata=("freezed"))]
pub struct DcbRecordItem {
/// 记录路径
pub path: String,
/// 记录索引
pub index: usize,
}
/// 检查数据是否为 DataForge/DCB 格式
pub fn dcb_is_dataforge(data: Vec<u8>) -> bool {
DataForge::is_dataforge(&data)
}
/// 从内存数据打开 DCB 文件
pub async fn dcb_open(data: Vec<u8>) -> Result<()> {
let df = tokio::task::spawn_blocking(move || {
DataForge::parse(&data).map_err(|e| anyhow!("Failed to parse DataForge: {}", e))
})
.await??;
*GLOBAL_DCB_READER.lock().unwrap() = Some(df);
Ok(())
}
/// 获取 DCB 记录数量
pub fn dcb_get_record_count() -> Result<usize> {
let reader = GLOBAL_DCB_READER.lock().unwrap();
let df = reader
.as_ref()
.ok_or_else(|| anyhow!("DCB reader not initialized"))?;
Ok(df.record_count())
}
/// 获取所有 DCB 记录路径列表
pub async fn dcb_get_record_list() -> Result<Vec<DcbRecordItem>> {
tokio::task::spawn_blocking(|| {
let reader = GLOBAL_DCB_READER.lock().unwrap();
let df = reader
.as_ref()
.ok_or_else(|| anyhow!("DCB reader not initialized"))?;
let path_to_record = df.path_to_record();
let mut result: Vec<DcbRecordItem> = path_to_record
.iter()
.map(|(path, &index)| DcbRecordItem {
path: path.clone(),
index,
})
.collect();
// 按路径排序
result.sort_by(|a, b| a.path.cmp(&b.path));
Ok(result)
})
.await?
}
/// 根据路径获取单条记录的 XML
pub async fn dcb_record_to_xml(path: String) -> Result<String> {
tokio::task::spawn_blocking(move || {
let reader = GLOBAL_DCB_READER.lock().unwrap();
let df = reader
.as_ref()
.ok_or_else(|| anyhow!("DCB reader not initialized"))?;
df.record_to_xml(&path, true)
.map_err(|e| anyhow!("Failed to convert record to XML: {}", e))
})
.await?
}
/// 根据索引获取单条记录的 XML
pub async fn dcb_record_to_xml_by_index(index: usize) -> Result<String> {
tokio::task::spawn_blocking(move || {
let reader = GLOBAL_DCB_READER.lock().unwrap();
let df = reader
.as_ref()
.ok_or_else(|| anyhow!("DCB reader not initialized"))?;
df.record_to_xml_by_index(index, true)
.map_err(|e| anyhow!("Failed to convert record to XML: {}", e))
})
.await?
}
/// 全文搜索 DCB 记录
/// 返回匹配的记录路径和预览摘要
#[frb(dart_metadata=("freezed"))]
pub struct DcbSearchResult {
/// 记录路径
pub path: String,
/// 记录索引
pub index: usize,
/// 匹配的行内容和行号列表
pub matches: Vec<DcbSearchMatch>,
}
#[frb(dart_metadata=("freezed"))]
pub struct DcbSearchMatch {
/// 行号从1开始
pub line_number: usize,
/// 匹配行的内容(带上下文)
pub line_content: String,
}
/// 全文搜索 DCB 记录
pub async fn dcb_search_all(query: String) -> Result<Vec<DcbSearchResult>> {
tokio::task::spawn_blocking(move || {
let reader = GLOBAL_DCB_READER.lock().unwrap();
let df = reader
.as_ref()
.ok_or_else(|| anyhow!("DCB reader not initialized"))?;
let query_lower = query.to_lowercase();
// 收集所有记录路径和索引
let records: Vec<(String, usize)> = df
.path_to_record()
.iter()
.map(|(path, &index)| (path.clone(), index))
.collect();
// 使用 rayon 并发搜索
let mut results: Vec<DcbSearchResult> = records
.par_iter()
.filter_map(|(path, index)| {
// 先检查路径是否匹配
let path_matches = path.to_lowercase().contains(&query_lower);
// 尝试获取 XML 并搜索内容
if let Ok(xml) = df.record_to_xml_by_index(*index, true) {
let mut matches = Vec::new();
for (line_num, line) in xml.lines().enumerate() {
if line.to_lowercase().contains(&query_lower) {
let line_content = if line.len() > 200 {
format!("{}...", &line[..200])
} else {
line.to_string()
};
matches.push(DcbSearchMatch {
line_number: line_num + 1,
line_content,
});
// 每条记录最多保留 5 个匹配
if matches.len() >= 5 {
break;
}
}
}
if path_matches || !matches.is_empty() {
return Some(DcbSearchResult {
path: path.clone(),
index: *index,
matches,
});
}
}
None
})
.collect();
// 按路径排序以保持结果稳定性
results.sort_by(|a, b| a.path.cmp(&b.path));
Ok(results)
})
.await?
}
/// 导出 DCB 到磁盘
/// merge: true = 合并为单个 XMLfalse = 分离为多个 XML 文件
pub async fn dcb_export_to_disk(output_path: String, dcb_path: String, merge: bool) -> Result<()> {
let output = PathBuf::from(&output_path);
let dcb = PathBuf::from(&dcb_path);
tokio::task::spawn_blocking(move || {
let reader = GLOBAL_DCB_READER.lock().unwrap();
let df = reader
.as_ref()
.ok_or_else(|| anyhow!("DCB reader not initialized"))?;
if merge {
unp4k::dataforge::export_merged(&df, &dcb, Some(&output))?;
} else {
unp4k::dataforge::export_separate(&df, &dcb, Some(&output))?;
}
Ok(())
})
.await?
}
/// 关闭 DCB 读取器
pub async fn dcb_close() -> Result<()> {
*GLOBAL_DCB_READER.lock().unwrap() = None;
Ok(())
}