client-core 0.1.0

Duck Client 核心库
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
// client-core/src/patch_executor/patch_processor.rs
//! 补丁包处理器
//!
//! 负责补丁包的下载、验证、解压等操作

use super::error::{PatchExecutorError, Result};
use crate::api_types::PatchPackageInfo;
use base64;
use flate2::read::GzDecoder;
use reqwest::Client;
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use tar::Archive;
use tempfile::TempDir;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tracing::{debug, info, warn};

/// 补丁包处理器
pub struct PatchProcessor {
    /// 临时工作目录
    temp_dir: TempDir,
    /// HTTP 客户端
    http_client: Client,
}

impl PatchProcessor {
    /// 创建新的补丁处理器
    pub fn new() -> Result<Self> {
        let temp_dir = TempDir::new()
            .map_err(|e| PatchExecutorError::custom(format!("Failed to create temp directory: {e}")))?;

        // 创建带超时的HTTP客户端
        let http_client = Client::builder()
            .timeout(std::time::Duration::from_secs(300)) // 5分钟超时
            .build()
            .map_err(|e| PatchExecutorError::custom(format!("Failed to create HTTP client: {e}")))?;

        debug!("Creating patch processor, temp directory: {:?}", temp_dir.path());

        Ok(Self {
            temp_dir,
            http_client,
        })
    }

    /// 下载补丁包
    pub async fn download_patch(&self, patch_info: &PatchPackageInfo) -> Result<PathBuf> {
        info!("Starting to download patch package: {}", patch_info.url);

        let patch_path = self.temp_dir.path().join("patch.tar.gz");

        // 发起HTTP请求
        let response = self
            .http_client
            .get(&patch_info.url)
            .send()
            .await
            .map_err(|e| PatchExecutorError::download_failed(format!("HTTP request failed: {e}")))?;

        if !response.status().is_success() {
            return Err(PatchExecutorError::download_failed(format!(
                "HTTP status code error: {}",
                response.status()
            )));
        }

        // 获取内容长度用于进度显示
        let total_size = response.content_length().unwrap_or(0);
        debug!("Patch package size: {} bytes", total_size);

        // 创建文件并写入数据
        let mut file = fs::File::create(&patch_path).await?;
        let mut downloaded = 0u64;

        let mut stream = response.bytes_stream();
        use futures_util::StreamExt;

        while let Some(chunk_result) = stream.next().await {
            let chunk = chunk_result
                .map_err(|e| PatchExecutorError::download_failed(format!("Failed to download data chunk: {e}")))?;

            file.write_all(&chunk).await?;
            downloaded += chunk.len() as u64;

            if total_size > 0 {
                let progress = (downloaded as f64 / total_size as f64) * 100.0;
                debug!("Download progress: {:.1}%", progress);
            }
        }

        file.flush().await?;
        info!("Patch package download completed: {:?} ({} bytes)", patch_path, downloaded);

        Ok(patch_path)
    }

    /// 验证补丁完整性
    pub async fn verify_patch_integrity(
        &self,
        patch_path: &Path,
        patch_info: &PatchPackageInfo,
    ) -> Result<()> {
        info!("Verifying patch integrity: {:?}", patch_path);

        // 1. 验证文件存在
        if !patch_path.exists() {
            return Err(PatchExecutorError::verification_failed("Patch file does not exist"));
        }

        // 2. 验证哈希值
        if let Some(hash) = &patch_info.hash {
            self.verify_hash(patch_path, hash).await?;
        }

        // 3. 验证数字签名
        if let Some(signature) = &patch_info.signature {
            self.verify_signature(patch_path, signature).await?;
        }

        info!("Patch integrity verification passed");
        Ok(())
    }

    /// 验证文件哈希
    async fn verify_hash(&self, file_path: &Path, expected_hash: &str) -> Result<()> {
        debug!("Verifying file hash: {:?}", file_path);

        // 解析期望的哈希值(格式:sha256:hash_value)
        let expected_hash = if expected_hash.starts_with("sha256:") {
            &expected_hash[7..]
        } else {
            expected_hash
        };

        // 计算文件的SHA256哈希
        let file_content = fs::read(file_path).await?;
        let mut hasher = Sha256::new();
        hasher.update(&file_content);
        let actual_hash = format!("{:x}", hasher.finalize());

        // 比较哈希值
        if actual_hash != expected_hash {
            return Err(PatchExecutorError::hash_mismatch(
                expected_hash.to_string(),
                actual_hash,
            ));
        }

        debug!("Hash verification passed: {}", actual_hash);
        Ok(())
    }

    /// 验证数字签名
    async fn verify_signature(&self, _file_path: &Path, signature: &str) -> Result<()> {
        debug!("Verifying digital signature: {}", signature);

        // TODO: 这里应该实现真正的数字签名验证
        // 目前只做基本的格式检查
        if signature.is_empty() {
            warn!("Digital signature is empty, skipping verification");
            return Ok(());
        }

        // 基本的base64格式检查
        use base64::{Engine as _, engine::general_purpose};
        if general_purpose::STANDARD.decode(signature).is_err() {
            return Err(PatchExecutorError::signature_verification_failed(
                "Signature is not a valid base64 format",
            ));
        }

        // TODO: 实际项目中需要:
        // 1. 解码签名
        // 2. 使用公钥验证签名
        // 3. 验证证书链

        debug!("Digital signature verification passed (simplified verification)");
        Ok(())
    }

    /// 解压补丁包
    pub async fn extract_patch(&self, patch_path: &Path) -> Result<PathBuf> {
        info!("Extracting patch package: {:?}", patch_path);

        let extract_dir = self.temp_dir.path().join("extracted");
        fs::create_dir_all(&extract_dir).await?;

        // 在阻塞任务中执行解压操作
        let patch_path_clone = patch_path.to_owned();
        let extract_dir_clone = extract_dir.clone();

        tokio::task::spawn_blocking(move || {
            Self::extract_tar_gz(&patch_path_clone, &extract_dir_clone)
        })
        .await
        .map_err(|e| PatchExecutorError::extraction_failed(format!("Extraction task failed: {e}")))??;

        info!("Patch package extracted: {:?}", extract_dir);
        Ok(extract_dir)
    }

    /// 解压tar.gz文件
    fn extract_tar_gz(archive_path: &Path, extract_to: &Path) -> Result<()> {
        let file = std::fs::File::open(archive_path)?;
        let decoder = GzDecoder::new(file);
        let mut archive = Archive::new(decoder);

        // 解压所有文件
        for entry_result in archive.entries()? {
            let mut entry = entry_result
                .map_err(|e| PatchExecutorError::extraction_failed(format!("Failed to read entry: {e}")))?;

            // 获取文件路径
            let path = entry.path().map_err(|e| {
                PatchExecutorError::extraction_failed(format!("Failed to get file path: {e}"))
            })?;

            // 将路径转换为PathBuf以避免借用问题
            let path_buf = path.to_path_buf();

            // 安全检查:防止路径遍历攻击
            if path_buf.is_absolute()
                || path_buf
                    .components()
                    .any(|c| c == std::path::Component::ParentDir)
            {
                return Err(PatchExecutorError::extraction_failed(format!(
                    "Unsafe file path: {path_buf:?}"
                )));
            }

            let extract_path = extract_to.join(&path_buf);

            // 确保父目录存在
            if let Some(parent) = extract_path.parent() {
                std::fs::create_dir_all(parent)?;
            }

            // 解压文件
            entry.unpack(&extract_path).map_err(|e| {
                PatchExecutorError::extraction_failed(format!("Failed to unpack file {path_buf:?}: {e}"))
            })?;

            debug!("Extracting file: {:?} -> {:?}", path_buf, extract_path);
        }

        Ok(())
    }

    /// 获取临时目录路径
    pub fn temp_dir(&self) -> &Path {
        self.temp_dir.path()
    }

    /// 获取解压目录中的文件列表
    pub async fn list_extracted_files(&self) -> Result<Vec<PathBuf>> {
        let extract_dir = self.temp_dir.path().join("extracted");

        if !extract_dir.exists() {
            return Ok(Vec::new());
        }

        let mut files = Vec::new();
        let mut read_dir = fs::read_dir(&extract_dir).await?;

        while let Some(entry) = read_dir.next_entry().await? {
            let path = entry.path();
            if path.is_file() {
                if let Ok(relative_path) = path.strip_prefix(&extract_dir) {
                    files.push(relative_path.to_owned());
                }
            }
        }

        Ok(files)
    }

    /// 验证解压后的文件结构
    pub async fn validate_extracted_structure(&self, required_files: &[String]) -> Result<()> {
        let extract_dir = self.temp_dir.path().join("extracted");

        for required_file in required_files {
            let file_path = extract_dir.join(required_file);
            if !file_path.exists() {
                return Err(PatchExecutorError::verification_failed(format!(
                    "Required file does not exist: {required_file}"
                )));
            }
        }

        debug!("Extracted file structure verification passed");
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use tokio::fs;

    #[tokio::test]
    async fn test_patch_processor_creation() {
        let processor = PatchProcessor::new();
        assert!(processor.is_ok());
    }

    #[tokio::test]
    async fn test_temp_dir_access() {
        let processor = PatchProcessor::new().unwrap();
        let temp_path = processor.temp_dir();
        assert!(temp_path.exists());
        assert!(temp_path.is_dir());
    }

    #[tokio::test]
    async fn test_hash_verification() {
        let processor = PatchProcessor::new().unwrap();

        // 创建测试文件
        let test_file = processor.temp_dir().join("test.txt");
        let content = b"hello world";
        fs::write(&test_file, content).await.unwrap();

        // 计算期望的哈希
        let mut hasher = Sha256::new();
        hasher.update(content);
        let expected_hash = format!("sha256:{:x}", hasher.finalize());

        // 验证哈希
        let result = processor.verify_hash(&test_file, &expected_hash).await;
        assert!(result.is_ok());

        // 测试错误的哈希
        let wrong_hash = "sha256:wronghash";
        let result = processor.verify_hash(&test_file, wrong_hash).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_signature_verification() {
        let processor = PatchProcessor::new().unwrap();
        let test_file = processor.temp_dir().join("test.txt");
        fs::write(&test_file, b"test").await.unwrap();

        // 测试有效的base64签名
        use base64::{Engine as _, engine::general_purpose};
        let valid_signature = general_purpose::STANDARD.encode("test signature");
        let result = processor
            .verify_signature(&test_file, &valid_signature)
            .await;
        assert!(result.is_ok());

        // 测试无效的签名
        let invalid_signature = "invalid!@#$%";
        let result = processor
            .verify_signature(&test_file, invalid_signature)
            .await;
        assert!(result.is_err());

        // 测试空签名
        let result = processor.verify_signature(&test_file, "").await;
        assert!(result.is_ok()); // 空签名会被跳过
    }

    #[tokio::test]
    async fn test_tar_gz_extraction() {
        let processor = PatchProcessor::new().unwrap();

        // 创建简单的tar.gz文件用于测试
        let tar_path = processor.temp_dir().join("test.tar.gz");
        let extract_dir = processor.temp_dir().join("extract_test");
        fs::create_dir_all(&extract_dir).await.unwrap();

        // 创建一个简单的tar.gz文件
        create_test_tar_gz(&tar_path).unwrap();

        // 测试解压
        let result = PatchProcessor::extract_tar_gz(&tar_path, &extract_dir);
        assert!(result.is_ok());

        // 验证文件已被解压
        let extracted_file = extract_dir.join("test.txt");
        assert!(extracted_file.exists());
    }

    #[tokio::test]
    async fn test_list_extracted_files() {
        let processor = PatchProcessor::new().unwrap();
        let extract_dir = processor.temp_dir().join("extracted");
        fs::create_dir_all(&extract_dir).await.unwrap();

        // 创建测试文件
        fs::write(extract_dir.join("file1.txt"), "content1")
            .await
            .unwrap();
        fs::write(extract_dir.join("file2.txt"), "content2")
            .await
            .unwrap();

        let files = processor.list_extracted_files().await.unwrap();
        assert_eq!(files.len(), 2);
        assert!(files.iter().any(|f| f.file_name().unwrap() == "file1.txt"));
        assert!(files.iter().any(|f| f.file_name().unwrap() == "file2.txt"));
    }

    #[tokio::test]
    async fn test_validate_extracted_structure() {
        let processor = PatchProcessor::new().unwrap();
        let extract_dir = processor.temp_dir().join("extracted");
        fs::create_dir_all(&extract_dir).await.unwrap();

        // 创建必需的文件
        fs::write(extract_dir.join("required1.txt"), "content")
            .await
            .unwrap();
        fs::write(extract_dir.join("required2.txt"), "content")
            .await
            .unwrap();

        let required_files = vec!["required1.txt".to_string(), "required2.txt".to_string()];
        let result = processor
            .validate_extracted_structure(&required_files)
            .await;
        assert!(result.is_ok());

        // 测试缺失文件
        let missing_files = vec!["missing.txt".to_string()];
        let result = processor.validate_extracted_structure(&missing_files).await;
        assert!(result.is_err());
    }

    // 辅助函数:创建测试用的tar.gz文件
    fn create_test_tar_gz(output_path: &Path) -> std::io::Result<()> {
        use flate2::Compression;
        use flate2::write::GzEncoder;

        let tar_gz = std::fs::File::create(output_path)?;
        let enc = GzEncoder::new(tar_gz, Compression::default());
        let mut tar = tar::Builder::new(enc);

        // 添加一个测试文件
        let mut header = tar::Header::new_gnu();
        header.set_path("test.txt")?;
        header.set_size(12);
        header.set_cksum();

        tar.append(&header, "hello world\n".as_bytes())?;
        tar.finish()?;

        Ok(())
    }
}