mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 00:55:15 +08:00
chore: establish development baseline
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "bat-assetbundle"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
# Unity AssetBundle 解析需要的依赖
|
||||
byteorder = "1.5"
|
||||
lz4 = "1.28"
|
||||
# LZMA 解压缩
|
||||
lzma-rs = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4"
|
||||
@@ -0,0 +1,26 @@
|
||||
//! AssetBundle 错误类型定义
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// AssetBundle 错误类型
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AssetBundleError {
|
||||
/// IO 错误
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// 解析错误
|
||||
#[error("Parse error: {0}")]
|
||||
Parse(String),
|
||||
|
||||
/// 不支持的格式
|
||||
#[error("Unsupported format: {0}")]
|
||||
UnsupportedFormat(String),
|
||||
|
||||
/// 其他错误
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// AssetBundle Result 类型
|
||||
pub type Result<T> = std::result::Result<T, AssetBundleError>;
|
||||
@@ -0,0 +1,17 @@
|
||||
//! # BAT AssetBundle
|
||||
//!
|
||||
//! Unity AssetBundle 解析器核心
|
||||
//!
|
||||
//! 提供插件化的 AssetBundle 解析框架
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![warn(clippy::all)]
|
||||
|
||||
pub mod error;
|
||||
pub mod parser;
|
||||
pub mod types;
|
||||
|
||||
pub use error::{AssetBundleError, Result};
|
||||
|
||||
/// AssetBundle 解析器版本号
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
@@ -0,0 +1,7 @@
|
||||
//! 解析器模块占位
|
||||
|
||||
/// AssetBundle 解析器接口(待实现)
|
||||
pub trait Parser {
|
||||
/// 解析器名称
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! AssetBundle 类型定义占位
|
||||
|
||||
/// Asset 类型(待实现)
|
||||
pub enum AssetType {
|
||||
/// 文本资源
|
||||
TextAsset,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "bat-cas-engine"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
blake3.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
||||
# 文件系统操作
|
||||
tokio = { workspace = true, features = ["fs", "io-util"] }
|
||||
|
||||
# 数据库(用于引用计数、元数据管理)
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
||||
|
||||
# Hex 编码
|
||||
hex = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
tempfile = "3.14"
|
||||
@@ -0,0 +1,35 @@
|
||||
//! CAS Engine 错误类型定义
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// CAS Engine 错误类型
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CasError {
|
||||
/// IO 错误
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Hash 不匹配
|
||||
#[error("Hash mismatch: expected {expected}, got {actual}")]
|
||||
HashMismatch {
|
||||
/// 期望的 Hash
|
||||
expected: String,
|
||||
/// 实际的 Hash
|
||||
actual: String,
|
||||
},
|
||||
|
||||
/// 对象不存在
|
||||
#[error("Object not found: {0}")]
|
||||
ObjectNotFound(String),
|
||||
|
||||
/// 数据库错误
|
||||
#[error("Database error: {0}")]
|
||||
Database(String),
|
||||
|
||||
/// 其他错误
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// CAS Engine Result 类型
|
||||
pub type Result<T> = std::result::Result<T, CasError>;
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Hash 计算模块
|
||||
//!
|
||||
//! 使用 BLAKE3 算法计算内容 Hash
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Hash 类型
|
||||
///
|
||||
/// 使用 BLAKE3 算法计算的 256 位 Hash
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Hash([u8; 32]);
|
||||
|
||||
impl Hash {
|
||||
/// 从字节数组创建 Hash
|
||||
pub fn from_bytes(bytes: [u8; 32]) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
/// 获取 Hash 的字节表示
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// 转换为十六进制字符串
|
||||
pub fn to_hex(&self) -> String {
|
||||
hex::encode(self.0)
|
||||
}
|
||||
|
||||
/// 从十六进制字符串解析
|
||||
pub fn from_hex(s: &str) -> Result<Self, crate::error::CasError> {
|
||||
let bytes = hex::decode(s).map_err(|_| {
|
||||
crate::error::CasError::Other(anyhow::anyhow!("Invalid hex string"))
|
||||
})?;
|
||||
|
||||
if bytes.len() != 32 {
|
||||
return Err(crate::error::CasError::Other(anyhow::anyhow!(
|
||||
"Hash must be 32 bytes"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut hash_bytes = [0u8; 32];
|
||||
hash_bytes.copy_from_slice(&bytes);
|
||||
Ok(Self(hash_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Hash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_hex())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Hash {
|
||||
type Err = crate::error::CasError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::from_hex(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for Hash {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_hex())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Hash {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Self::from_hex(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算数据的 BLAKE3 Hash
|
||||
///
|
||||
/// # 参数
|
||||
/// - `data`: 要计算 Hash 的数据
|
||||
///
|
||||
/// # 返回
|
||||
/// - 数据的 BLAKE3 Hash
|
||||
///
|
||||
/// # 示例
|
||||
/// ```
|
||||
/// use bat_cas_engine::hash::compute_hash;
|
||||
///
|
||||
/// let data = b"Hello, World!";
|
||||
/// let hash = compute_hash(data);
|
||||
/// println!("Hash: {}", hash);
|
||||
/// ```
|
||||
pub fn compute_hash(data: &[u8]) -> Hash {
|
||||
let hash_bytes = blake3::hash(data);
|
||||
Hash::from_bytes(*hash_bytes.as_bytes())
|
||||
}
|
||||
|
||||
/// 计算文件的 BLAKE3 Hash
|
||||
///
|
||||
/// # 参数
|
||||
/// - `path`: 文件路径
|
||||
///
|
||||
/// # 返回
|
||||
/// - 成功:返回文件的 Hash
|
||||
/// - 失败:返回错误
|
||||
pub async fn compute_file_hash(path: &std::path::Path) -> crate::error::Result<Hash> {
|
||||
let data = tokio::fs::read(path).await?;
|
||||
Ok(compute_hash(&data))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_compute_hash() {
|
||||
let data = b"Hello, World!";
|
||||
let hash = compute_hash(data);
|
||||
|
||||
// BLAKE3 Hash 应该是确定性的
|
||||
let hash2 = compute_hash(data);
|
||||
assert_eq!(hash, hash2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_to_string() {
|
||||
let data = b"Test data";
|
||||
let hash = compute_hash(data);
|
||||
let hex = hash.to_hex();
|
||||
|
||||
// 应该是 64 个字符的十六进制字符串
|
||||
assert_eq!(hex.len(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_from_string() {
|
||||
let data = b"Test data";
|
||||
let hash = compute_hash(data);
|
||||
let hex = hash.to_hex();
|
||||
|
||||
let parsed = Hash::from_hex(&hex).unwrap();
|
||||
assert_eq!(hash, parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_different_data() {
|
||||
let hash1 = compute_hash(b"Data 1");
|
||||
let hash2 = compute_hash(b"Data 2");
|
||||
|
||||
assert_ne!(hash1, hash2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_serialization() {
|
||||
let data = b"Serialization test";
|
||||
let hash = compute_hash(data);
|
||||
|
||||
let json = serde_json::to_string(&hash).unwrap();
|
||||
let deserialized: Hash = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(hash, deserialized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! # BAT CAS Engine
|
||||
//!
|
||||
//! Content Addressable Storage (CAS) 引擎核心实现
|
||||
//!
|
||||
//! 提供基于内容寻址的存储系统,支持:
|
||||
//! - Hash 去重
|
||||
//! - 引用计数
|
||||
//! - 垃圾回收
|
||||
//! - 多版本共享
|
||||
//! - 完整性校验
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![warn(clippy::all)]
|
||||
|
||||
pub mod error;
|
||||
pub mod storage;
|
||||
pub mod hash;
|
||||
pub mod refcount;
|
||||
|
||||
pub use error::{CasError, Result};
|
||||
|
||||
/// CAS Engine 版本号
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
assert!(!VERSION.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! 引用计数模块占位
|
||||
|
||||
/// 引用计数管理(待实现)
|
||||
pub struct RefCounter;
|
||||
@@ -0,0 +1,299 @@
|
||||
//! CAS 存储接口定义
|
||||
//!
|
||||
//! 提供统一的 Content Addressable Storage 抽象接口
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::hash::Hash;
|
||||
use async_trait::async_trait;
|
||||
use std::path::Path;
|
||||
|
||||
/// CAS 存储接口
|
||||
///
|
||||
/// 所有 CAS 存储实现必须实现此 trait
|
||||
#[async_trait]
|
||||
pub trait Storage: Send + Sync {
|
||||
/// 存储对象
|
||||
///
|
||||
/// # 参数
|
||||
/// - `data`: 要存储的数据
|
||||
///
|
||||
/// # 返回
|
||||
/// - 成功:返回对象的 Hash
|
||||
/// - 失败:返回错误
|
||||
async fn put(&self, data: &[u8]) -> Result<Hash>;
|
||||
|
||||
/// 获取对象
|
||||
///
|
||||
/// # 参数
|
||||
/// - `hash`: 对象的 Hash
|
||||
///
|
||||
/// # 返回
|
||||
/// - 成功:返回对象数据
|
||||
/// - 失败:如果对象不存在或读取失败,返回错误
|
||||
async fn get(&self, hash: &Hash) -> Result<Vec<u8>>;
|
||||
|
||||
/// 检查对象是否存在
|
||||
///
|
||||
/// # 参数
|
||||
/// - `hash`: 对象的 Hash
|
||||
///
|
||||
/// # 返回
|
||||
/// - 存在返回 true,否则返回 false
|
||||
async fn exists(&self, hash: &Hash) -> bool;
|
||||
|
||||
/// 删除对象
|
||||
///
|
||||
/// # 参数
|
||||
/// - `hash`: 对象的 Hash
|
||||
///
|
||||
/// # 返回
|
||||
/// - 成功:返回 Ok(())
|
||||
/// - 失败:返回错误
|
||||
async fn delete(&self, hash: &Hash) -> Result<()>;
|
||||
|
||||
/// 获取对象大小
|
||||
///
|
||||
/// # 参数
|
||||
/// - `hash`: 对象的 Hash
|
||||
///
|
||||
/// # 返回
|
||||
/// - 成功:返回对象大小(字节)
|
||||
/// - 失败:返回错误
|
||||
async fn size(&self, hash: &Hash) -> Result<u64>;
|
||||
|
||||
/// 列出所有对象
|
||||
///
|
||||
/// # 返回
|
||||
/// - 所有对象的 Hash 列表
|
||||
async fn list(&self) -> Result<Vec<Hash>>;
|
||||
|
||||
/// 获取存储统计信息
|
||||
///
|
||||
/// # 返回
|
||||
/// - 存储统计信息
|
||||
async fn stats(&self) -> Result<StorageStats>;
|
||||
}
|
||||
|
||||
/// 存储统计信息
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StorageStats {
|
||||
/// 对象总数
|
||||
pub object_count: u64,
|
||||
|
||||
/// 总存储大小(字节)
|
||||
pub total_size: u64,
|
||||
|
||||
/// 存储路径
|
||||
pub storage_path: String,
|
||||
}
|
||||
|
||||
/// 文件系统 CAS 存储实现
|
||||
pub struct FileSystemStorage {
|
||||
/// 存储根目录
|
||||
root: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl FileSystemStorage {
|
||||
/// 创建新的文件系统存储
|
||||
///
|
||||
/// # 参数
|
||||
/// - `root`: 存储根目录
|
||||
///
|
||||
/// # 返回
|
||||
/// - 成功:返回存储实例
|
||||
/// - 失败:返回错误
|
||||
pub async fn new(root: impl AsRef<Path>) -> Result<Self> {
|
||||
let root = root.as_ref().to_path_buf();
|
||||
|
||||
// 创建根目录
|
||||
tokio::fs::create_dir_all(&root).await.map_err(|e| {
|
||||
crate::error::CasError::Io(e)
|
||||
})?;
|
||||
|
||||
// 创建 objects 目录
|
||||
let objects_dir = root.join("objects");
|
||||
tokio::fs::create_dir_all(&objects_dir).await.map_err(|e| {
|
||||
crate::error::CasError::Io(e)
|
||||
})?;
|
||||
|
||||
Ok(Self { root })
|
||||
}
|
||||
|
||||
/// 获取对象文件路径
|
||||
///
|
||||
/// 使用 Hash 的前两个字符作为子目录,避免单目录文件过多
|
||||
///
|
||||
/// 例如:hash = "abcdef1234..." -> objects/ab/cdef1234...
|
||||
fn object_path(&self, hash: &Hash) -> std::path::PathBuf {
|
||||
let hash_str = hash.to_string();
|
||||
let (prefix, suffix) = hash_str.split_at(2);
|
||||
self.root.join("objects").join(prefix).join(suffix)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Storage for FileSystemStorage {
|
||||
async fn put(&self, data: &[u8]) -> Result<Hash> {
|
||||
// 计算 Hash
|
||||
let hash = crate::hash::compute_hash(data);
|
||||
|
||||
// 检查是否已存在
|
||||
if self.exists(&hash).await {
|
||||
return Ok(hash);
|
||||
}
|
||||
|
||||
// 获取文件路径
|
||||
let path = self.object_path(&hash);
|
||||
|
||||
// 创建父目录
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
tokio::fs::write(&path, data).await?;
|
||||
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
async fn get(&self, hash: &Hash) -> Result<Vec<u8>> {
|
||||
let path = self.object_path(hash);
|
||||
|
||||
// 读取文件
|
||||
let data = tokio::fs::read(&path).await.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
crate::error::CasError::ObjectNotFound(hash.to_string())
|
||||
} else {
|
||||
crate::error::CasError::Io(e)
|
||||
}
|
||||
})?;
|
||||
|
||||
// 验证 Hash
|
||||
let computed_hash = crate::hash::compute_hash(&data);
|
||||
if computed_hash != *hash {
|
||||
return Err(crate::error::CasError::HashMismatch {
|
||||
expected: hash.to_string(),
|
||||
actual: computed_hash.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn exists(&self, hash: &Hash) -> bool {
|
||||
let path = self.object_path(hash);
|
||||
tokio::fs::try_exists(&path).await.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn delete(&self, hash: &Hash) -> Result<()> {
|
||||
let path = self.object_path(hash);
|
||||
tokio::fs::remove_file(&path).await.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
crate::error::CasError::ObjectNotFound(hash.to_string())
|
||||
} else {
|
||||
crate::error::CasError::Io(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn size(&self, hash: &Hash) -> Result<u64> {
|
||||
let path = self.object_path(hash);
|
||||
let metadata = tokio::fs::metadata(&path).await.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
crate::error::CasError::ObjectNotFound(hash.to_string())
|
||||
} else {
|
||||
crate::error::CasError::Io(e)
|
||||
}
|
||||
})?;
|
||||
Ok(metadata.len())
|
||||
}
|
||||
|
||||
async fn list(&self) -> Result<Vec<Hash>> {
|
||||
let objects_dir = self.root.join("objects");
|
||||
let mut hashes = Vec::new();
|
||||
|
||||
// 遍历所有子目录
|
||||
let mut read_dir = tokio::fs::read_dir(&objects_dir).await?;
|
||||
while let Some(entry) = read_dir.next_entry().await? {
|
||||
let prefix_path = entry.path();
|
||||
if !prefix_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 遍历子目录中的文件
|
||||
let mut sub_read_dir = tokio::fs::read_dir(&prefix_path).await?;
|
||||
while let Some(file_entry) = sub_read_dir.next_entry().await? {
|
||||
if let Some(file_name) = file_entry.file_name().to_str() {
|
||||
if let Some(prefix) = prefix_path.file_name().and_then(|p| p.to_str()) {
|
||||
let hash_str = format!("{}{}", prefix, file_name);
|
||||
if let Ok(hash) = hash_str.parse() {
|
||||
hashes.push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hashes)
|
||||
}
|
||||
|
||||
async fn stats(&self) -> Result<StorageStats> {
|
||||
let hashes = self.list().await?;
|
||||
let mut total_size = 0u64;
|
||||
|
||||
for hash in &hashes {
|
||||
if let Ok(size) = self.size(hash).await {
|
||||
total_size += size;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(StorageStats {
|
||||
object_count: hashes.len() as u64,
|
||||
total_size,
|
||||
storage_path: self.root.display().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_put_and_get() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let storage = FileSystemStorage::new(temp_dir.path()).await.unwrap();
|
||||
|
||||
let data = b"Hello, CAS!";
|
||||
let hash = storage.put(data).await.unwrap();
|
||||
|
||||
let retrieved = storage.get(&hash).await.unwrap();
|
||||
assert_eq!(data, retrieved.as_slice());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deduplication() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let storage = FileSystemStorage::new(temp_dir.path()).await.unwrap();
|
||||
|
||||
let data = b"Same content";
|
||||
let hash1 = storage.put(data).await.unwrap();
|
||||
let hash2 = storage.put(data).await.unwrap();
|
||||
|
||||
assert_eq!(hash1, hash2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_exists() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let storage = FileSystemStorage::new(temp_dir.path()).await.unwrap();
|
||||
|
||||
let data = b"Test data";
|
||||
let hash = storage.put(data).await.unwrap();
|
||||
|
||||
assert!(storage.exists(&hash).await);
|
||||
|
||||
storage.delete(&hash).await.unwrap();
|
||||
assert!(!storage.exists(&hash).await);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "bat-ffi"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
bat-cas-engine = { path = "../bat-cas-engine" }
|
||||
bat-assetbundle = { path = "../bat-assetbundle" }
|
||||
bat-patch = { path = "../bat-patch" }
|
||||
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
# FFI 绑定
|
||||
libc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.27"
|
||||
@@ -0,0 +1,50 @@
|
||||
//! # BAT FFI
|
||||
//!
|
||||
//! Go 和 Rust 之间的 FFI 绑定层
|
||||
//!
|
||||
//! 导出 C ABI 接口供 Go 通过 CGO 调用
|
||||
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
/// FFI 版本号
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// 获取版本号(C ABI)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bat_version() -> *const c_char {
|
||||
CString::new(VERSION).unwrap().into_raw()
|
||||
}
|
||||
|
||||
/// 释放 C 字符串内存
|
||||
///
|
||||
/// # Safety
|
||||
/// 调用者必须确保:
|
||||
/// - `s` 是通过 `bat_version` 返回的有效指针
|
||||
/// - `s` 只被释放一次
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn bat_free_string(s: *mut c_char) {
|
||||
if !s.is_null() {
|
||||
let _ = CString::from_raw(s);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::CStr;
|
||||
|
||||
#[test]
|
||||
fn test_ffi_version() {
|
||||
let version_ptr = bat_version();
|
||||
assert!(!version_ptr.is_null());
|
||||
|
||||
unsafe {
|
||||
let version_cstr = CStr::from_ptr(version_ptr);
|
||||
let version = version_cstr.to_str().unwrap();
|
||||
assert!(!version.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "bat-patch"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
blake3.workspace = true
|
||||
|
||||
# Binary diff/patch
|
||||
bsdiff = "0.2"
|
||||
|
||||
# JSON patch
|
||||
json-patch = "4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.14"
|
||||
@@ -0,0 +1,6 @@
|
||||
//! Binary Patch 模块占位
|
||||
|
||||
/// Binary Patch 应用(待实现)
|
||||
pub fn apply_patch(_old: &[u8], _patch: &[u8]) -> crate::Result<Vec<u8>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//! Patch 错误类型定义
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Patch 错误类型
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PatchError {
|
||||
/// IO 错误
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Patch 应用失败
|
||||
#[error("Patch apply failed: {0}")]
|
||||
ApplyFailed(String),
|
||||
|
||||
/// 其他错误
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Patch Result 类型
|
||||
pub type Result<T> = std::result::Result<T, PatchError>;
|
||||
@@ -0,0 +1,6 @@
|
||||
//! JSON Patch 模块占位
|
||||
|
||||
/// JSON Patch 应用(待实现)
|
||||
pub fn apply_json_patch(_doc: &str, _patch: &str) -> crate::Result<String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! # BAT Patch
|
||||
//!
|
||||
//! Patch 引擎核心实现
|
||||
//!
|
||||
//! 支持:
|
||||
//! - Binary Diff/Patch
|
||||
//! - JSON Patch
|
||||
//! - Rollback
|
||||
//! - Integrity Check
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![warn(clippy::all)]
|
||||
|
||||
pub mod error;
|
||||
pub mod binary;
|
||||
pub mod json;
|
||||
|
||||
pub use error::{PatchError, Result};
|
||||
|
||||
/// Patch 引擎版本号
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
Reference in New Issue
Block a user