mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-14 04:00:27 +00:00
427 lines
13 KiB
Rust
427 lines
13 KiB
Rust
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 = 合并为单个 XML,false = 分离为多个 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(())
|
||
}
|