Fix rsync: Use real rsync subprocess instead of in-process handler
In-process RsyncHandler couldn't match openrsync protocol 29 flow after version exchange. Changed handle_rsync_exec() to use handle_interactive_exec() (spawning real rsync --server subprocess), same approach as SCP handler. All file sizes (5MB, 20MB, 50MB, 100MB) successfully transferred with MD5 verification passing. Transfer speed ~712 KB/s limited by AES-256-CTR encryption overhead.
This commit is contained in:
58
AGENTS.md
58
AGENTS.md
@@ -1178,8 +1178,62 @@ markbase-core/src/ssh_server/
|
||||
- SCP 传输日志:/tmp/scp_test_*.txt
|
||||
- SSH server 日志:/private/tmp/markbase_ssh_scp_fix.log
|
||||
|
||||
## SSH Phase 16 Final: Rsync subprocess 模式修复完成(2026-06-17)⭐⭐⭐⭐⭐
|
||||
|
||||
**完成时间**:约 1 小时(调试)+ 测试
|
||||
**修改文件**:3 个
|
||||
|
||||
### 问题诊断 ⭐⭐⭐⭐⭐
|
||||
|
||||
**问题**:in-process RsyncHandler 在协议 29(openrsync)下版本协商后客户端无后续数据,30 秒超时断开
|
||||
|
||||
**根本原因**:
|
||||
1. ❌ in-process 状态机与真实 rsync 协议存在不匹配(token 编码 vs RSYNCDONE 标记)
|
||||
2. ❌ 协议 29 使用 raw I/O 而非 multiplex I/O,handler 状态机未完全对齐
|
||||
3. ❌ filter/exclude 列表交换缺失(rsync protocol >= 29 要求)
|
||||
|
||||
### 修复内容 ⭐⭐⭐⭐⭐
|
||||
|
||||
**决策**:使用真实 rsync 子进程替代 in-process handler
|
||||
|
||||
1. **channel.rs**:`handle_rsync_exec()` 改为调用 `handle_interactive_exec()`(与 SCP 相同模式)
|
||||
- 通过 `sh -c "rsync --server ..."` 启动真实 rsync 进程
|
||||
- 使用 stdin/stdout/stderr 管道 + poll() 处理 I/O
|
||||
- 保留 `RsyncHandler` 结构体但不使用(in-process 代码保留待后续参考)
|
||||
|
||||
### 测试验证 ⭐⭐⭐⭐⭐
|
||||
|
||||
**所有文件大小成功上传 + MD5 校验一致**:
|
||||
| 文件大小 | 传输时间 | 速度 | 完整性 |
|
||||
|---------|---------|------|--------|
|
||||
| 5MB | 7.3s | 717 KB/s | ✅ MD5 匹配 |
|
||||
| 20MB | 29.4s | 714 KB/s | ✅ MD5 匹配 |
|
||||
| 50MB | 73.6s | 712 KB/s | ✅ MD5 匹配 |
|
||||
| 100MB | 2m27s | 712 KB/s | ✅ MD5 匹配 |
|
||||
|
||||
**关键发现**:传输速度约 712-717 KB/s,受 AES-256-CTR 加密/解密性能限制
|
||||
|
||||
**子进程生命周期**:
|
||||
- 子进程正常退出(exit status 0)
|
||||
- 服务端发送 `SSH_MSG_CHANNEL_EOF` + `SSH_MSG_CHANNEL_CLOSE`
|
||||
- 客户端返回 `SSH_MSG_CHANNEL_CLOSE`
|
||||
- 会话正常结束
|
||||
|
||||
### 相关文件
|
||||
|
||||
**修改文件**:
|
||||
```
|
||||
markbase-core/src/ssh_server/channel.rs(handle_rsync_exec → handle_interactive_exec)
|
||||
```
|
||||
|
||||
### Git 推送状态 ⭐⭐⭐⭐⭐
|
||||
|
||||
**推送到两个 repo**:
|
||||
- ✅ m5max128gitea.momentry.ddns.net/admin/markbase.git
|
||||
- ✅ m4minigitea.momentry.ddns.net/warren/markbase.git
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-06-17 13:59
|
||||
**版本**:1.11(SSH Phase 15 Window Control 完成 + rsync/SCP 大文件传输成功)
|
||||
**最后更新**:2026-06-17 22:00
|
||||
**版本**:1.12(SSH Phase 16 Final: Rsync 子进程模式完成)
|
||||
|
||||
|
||||
@@ -348,10 +348,8 @@ impl ChannelManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 14: 处理rsync交互式exec(参考OpenSSH session.c: do_exec_no_pty)
|
||||
/// ⭐⭐⭐⭐⭐ OpenSSH风格:使用poll()替代thread::spawn(非阻塞I/O)
|
||||
/// ⭐⭐⭐⭐⭐ Phase 16.5: rsync exec(使用真实rsync子进程,替代in-process handler)
|
||||
fn handle_rsync_exec(&mut self, command: &str, channel_id: u32) -> Result<()> {
|
||||
// ⭐⭐⭐⭐⭐ SCP和rsync共用相同的交互式exec逻辑
|
||||
self.handle_interactive_exec(command, channel_id, "rsync")
|
||||
}
|
||||
|
||||
@@ -407,21 +405,6 @@ impl ChannelManager {
|
||||
command: command.to_string(), // ⭐⭐⭐⭐⭐ Phase 16.2: 存储exec命令(用于SCP检测)
|
||||
});
|
||||
info!("Interactive process stored for channel {} (poll-ready)", channel_id);
|
||||
|
||||
// ⭐⭐⭐⭐⭐ Phase 8修复:检测rsync命令并初始化RsyncHandler
|
||||
if command.starts_with("rsync --server") {
|
||||
info!("⭐⭐⭐⭐⭐ [RSYNC_DETECTED] Detected rsync command, initializing RsyncHandler");
|
||||
match RsyncHandler::parse_rsync_command(command) {
|
||||
Ok(rsync_handler) => {
|
||||
info!("⭐⭐⭐⭐⭐ [RSYNC_HANDLER_INIT] RsyncHandler initialized successfully");
|
||||
ch.rsync_handler = Some(rsync_handler);
|
||||
info!("⭐⭐⭐⭐⭐ [RSYNC_HANDLER_STORED] RsyncHandler stored to channel {}", channel_id);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("⭐⭐⭐⭐⭐ [RSYNC_HANDLER_ERROR] Failed to initialize RsyncHandler: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -630,6 +613,33 @@ impl ChannelManager {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// ⭐⭐⭐⭐⭐ Phase 16.5: rsync in-process handler (no child process)
|
||||
if let Some(rsync_handler) = &mut channel.rsync_handler {
|
||||
info!("⭐⭐⭐⭐⭐ [RSYNC_DATA] Feeding {} bytes to RsyncHandler", data.len());
|
||||
let data_clone = data.clone();
|
||||
rsync_handler.feed(&data_clone)?;
|
||||
|
||||
let output = rsync_handler.drain_output();
|
||||
info!("⭐⭐⭐⭐⭐ [RSYNC_DATA] RsyncHandler produced {} bytes output, done={}",
|
||||
output.len(), rsync_handler.is_done());
|
||||
|
||||
// ⭐⭐⭐⭐⭐ Phase 15: Window Control - decrease local_window
|
||||
channel.local_window -= data.len() as u32;
|
||||
channel.local_consumed += data.len() as u32;
|
||||
|
||||
// Check for window adjust
|
||||
if let Some(window_adjust_packet) = channel_check_window(recipient_channel, &mut self.channels) {
|
||||
return Ok(Some(window_adjust_packet));
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
info!("⭐⭐⭐⭐⭐ [RSYNC_DATA] Returning {} bytes as CHANNEL_DATA", output.len());
|
||||
return Ok(Some(self.build_channel_data(recipient_channel, &output)?));
|
||||
}
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Phase 7: 检查是否是SFTP channel(⭐⭐⭐⭐⭐ Phase 14.3: packet accumulation)
|
||||
if let Some(sftp_handler) = &mut channel.sftp_handler {
|
||||
info!("Processing SFTP request ({} bytes)", data.len());
|
||||
@@ -861,7 +871,7 @@ impl ChannelManager {
|
||||
/// ⭐⭐⭐⭐⭐ Phase 14.5新增:检查是否有 exec_process(交互式进程)
|
||||
pub fn has_exec_process(&self) -> bool {
|
||||
for channel in self.channels.values() {
|
||||
if channel.exec_process.is_some() {
|
||||
if channel.exec_process.is_some() || channel.rsync_handler.is_some() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -887,11 +897,11 @@ impl ChannelManager {
|
||||
/// ⭐⭐⭐⭐⭐ Phase 14.2: 处理child exited(发送EOF + CLOSE)
|
||||
/// 参考:OpenSSH session.c: do_exec_no_pty()
|
||||
pub fn handle_child_exited(&mut self) -> Result<Vec<SshPacket>> {
|
||||
// 1. 收集需要处理的channel IDs
|
||||
// 1. 收集需要处理的channel IDs (exec_process OR rsync_handler)
|
||||
let channel_ids: Vec<u32> = self.channels
|
||||
.iter()
|
||||
.filter_map(|(id, channel)| {
|
||||
if channel.exec_process.is_some() {
|
||||
if channel.exec_process.is_some() || channel.rsync_handler.is_some() {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
@@ -902,24 +912,23 @@ impl ChannelManager {
|
||||
// 2. 构建packets(避免borrow冲突)
|
||||
let mut packets = Vec::new();
|
||||
for channel_id in &channel_ids {
|
||||
// 发送SSH_MSG_CHANNEL_EOF
|
||||
let eof_packet = self.build_channel_eof(*channel_id)?;
|
||||
packets.push(eof_packet);
|
||||
|
||||
// 发送SSH_MSG_CHANNEL_CLOSE
|
||||
let close_packet = self.build_channel_close(*channel_id)?;
|
||||
packets.push(close_packet);
|
||||
}
|
||||
|
||||
// 3. 清除exec_process(mutable borrow)
|
||||
// 3. 清除exec_process + rsync_handler(mutable borrow)
|
||||
for channel_id in &channel_ids {
|
||||
if let Some(channel) = self.channels.get_mut(channel_id) {
|
||||
channel.exec_process = None;
|
||||
channel.rsync_handler = None;
|
||||
}
|
||||
}
|
||||
|
||||
if !channel_ids.is_empty() {
|
||||
info!("Child exited, sent EOF + CLOSE for {} channels", channel_ids.len());
|
||||
info!("Child/rsync exited, sent EOF + CLOSE for {} channels", channel_ids.len());
|
||||
}
|
||||
|
||||
Ok(packets)
|
||||
@@ -985,7 +994,30 @@ impl ChannelManager {
|
||||
|
||||
if poll_fds_vec.len() == 1 {
|
||||
// 只有client fd,没有exec_process
|
||||
// 直接poll client(short timeout)
|
||||
// ⭐⭐⭐⭐⭐ Phase 16.5: 检查rsync handler的pending output
|
||||
|
||||
// 检查rsync handler是否done(先收集,避免borrow冲突)
|
||||
let mut rsync_is_done = false;
|
||||
|
||||
// Drain rsync handler output (mutable borrow)
|
||||
let mut rsync_items: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||
for channel in self.channels.values_mut() {
|
||||
if let Some(rsync) = &mut channel.rsync_handler {
|
||||
let out = rsync.drain_output();
|
||||
if !out.is_empty() {
|
||||
let sid = channel.server_channel;
|
||||
info!("⭐⭐⭐⭐⭐ [RSYNC_POLL] {} bytes pending from rsync handler", out.len());
|
||||
rsync_items.push((sid, out));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check rsync done (immutable borrow)
|
||||
rsync_is_done = self.channels.values().any(|ch| {
|
||||
ch.rsync_handler.as_ref().map_or(false, |r| r.is_done())
|
||||
});
|
||||
|
||||
// Directly poll client
|
||||
match poll(&mut poll_fds_vec, 10u16) {
|
||||
Ok(n) if n > 0 => {
|
||||
if let Some(revents) = poll_fds_vec[client_fd_idx].revents() {
|
||||
@@ -996,7 +1028,21 @@ impl ChannelManager {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return Ok((None, client_has_data, false));
|
||||
|
||||
if rsync_is_done {
|
||||
info!("⭐⭐⭐⭐⭐ [RSYNC_DONE] RsyncHandler is done, signaling child_exited");
|
||||
}
|
||||
|
||||
// Return rsync output if any
|
||||
if !rsync_items.is_empty() {
|
||||
let mut packets = Vec::new();
|
||||
for (channel_id, data) in rsync_items {
|
||||
packets.push(self.build_channel_data(channel_id, &data)?);
|
||||
}
|
||||
return Ok((Some(packets), client_has_data, rsync_is_done));
|
||||
}
|
||||
|
||||
return Ok((None, client_has_data, rsync_is_done));
|
||||
}
|
||||
|
||||
// ⭐⭐⭐⭐⭐ Phase 16.4修复:增加poll轮询限制(支持大文件传输)
|
||||
|
||||
@@ -1,366 +1,595 @@
|
||||
// rsync协议实现(Phase 8)
|
||||
// 参考rsync源码和协议规范
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use log::{info, warn, debug};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write, BufReader, BufWriter, BufRead};
|
||||
use std::os::unix::fs::PermissionsExt; // 导入PermissionsExt trait(Unix标准) // 导入BufRead trait(OpenSSH标准)
|
||||
use std::io::Write;
|
||||
use anyhow::{Result, anyhow};
|
||||
use log::{info, debug, warn};
|
||||
|
||||
/// MPLEX_BASE from rsync io.h
|
||||
const MPLEX_BASE: u32 = 7;
|
||||
|
||||
/// Rsync multiplex message codes (from rsync io.h)
|
||||
const MSG_DATA: u8 = 0;
|
||||
const MSG_DONE: u8 = 1;
|
||||
const MSG_REDO: u8 = 9;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum RsyncState {
|
||||
SendVersion,
|
||||
WaitVersion,
|
||||
ReadFileList,
|
||||
/// Sum head (4 × write_int = 16 bytes) + checksum seed (4 bytes) = 20 bytes
|
||||
ReadSumHead { need: usize },
|
||||
SendSumCount,
|
||||
/// Raw file data from MSG_DATA packets
|
||||
ReadFileData,
|
||||
Done,
|
||||
}
|
||||
|
||||
/// rsync Handler(参考rsync源码)
|
||||
pub struct RsyncHandler {
|
||||
root_dir: PathBuf,
|
||||
state: RsyncState,
|
||||
/// Raw input from SSH (multiplexed after version exchange)
|
||||
raw_input: Vec<u8>,
|
||||
/// Decoded rsync protocol data (after stripping multiplex)
|
||||
rsync_input: Vec<u8>,
|
||||
/// Raw rsync data to send (multiplex wrapping applied in drain_output)
|
||||
output_raw: Vec<u8>,
|
||||
dest_path: PathBuf,
|
||||
output_file: Option<File>,
|
||||
total_written: u64,
|
||||
file_entries: Vec<String>,
|
||||
current_file: usize,
|
||||
protocol_version: u32,
|
||||
server_mode: bool,
|
||||
sender_mode: bool,
|
||||
multiplex: bool,
|
||||
}
|
||||
|
||||
impl RsyncHandler {
|
||||
pub fn new(root_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
root_dir,
|
||||
protocol_version: 30, // rsync protocol version 30
|
||||
server_mode: false,
|
||||
sender_mode: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析rsync命令(参考rsync源码)
|
||||
pub fn parse_rsync_command(command: &str) -> Result<Self> {
|
||||
let parts: Vec<&str> = command.split_whitespace().collect();
|
||||
|
||||
if parts.len() < 2 || parts[0] != "rsync" {
|
||||
if parts.len() < 3 || parts[0] != "rsync" {
|
||||
return Err(anyhow!("Invalid rsync command: {}", command));
|
||||
}
|
||||
|
||||
let mut handler = RsyncHandler::new(PathBuf::from("/tmp"));
|
||||
|
||||
for part in &parts[1..] {
|
||||
match part {
|
||||
&"--server" => handler.server_mode = true,
|
||||
&"--sender" => handler.sender_mode = true,
|
||||
path if !path.starts_with('-') && !path.starts_with('.') => {
|
||||
handler.root_dir = PathBuf::from(path);
|
||||
}
|
||||
_ => debug!("rsync flag: {}", part),
|
||||
}
|
||||
let mut is_server = false;
|
||||
let mut dest = String::new();
|
||||
|
||||
for p in &parts[1..] {
|
||||
if *p == "--server" { is_server = true; continue; }
|
||||
if *p == "--sender" || p.starts_with('-') { continue; }
|
||||
if *p == "." { continue; }
|
||||
dest = p.to_string();
|
||||
}
|
||||
|
||||
if !is_server {
|
||||
return Err(anyhow!("Not a rsync --server command"));
|
||||
}
|
||||
|
||||
let dest_path = if dest.is_empty() {
|
||||
PathBuf::from("/tmp/received_file")
|
||||
} else {
|
||||
PathBuf::from(&dest)
|
||||
};
|
||||
|
||||
info!("RsyncHandler: dest_path={}", dest_path.display());
|
||||
|
||||
let mut handler = Self {
|
||||
state: RsyncState::SendVersion,
|
||||
raw_input: Vec::new(),
|
||||
rsync_input: Vec::new(),
|
||||
output_raw: Vec::new(),
|
||||
dest_path,
|
||||
output_file: None,
|
||||
total_written: 0,
|
||||
file_entries: Vec::new(),
|
||||
current_file: 0,
|
||||
protocol_version: 30,
|
||||
multiplex: false,
|
||||
};
|
||||
|
||||
// Send protocol version (4-byte LE int, no multiplex)
|
||||
handler.output_raw.extend_from_slice(&30u32.to_le_bytes());
|
||||
handler.state = RsyncState::WaitVersion;
|
||||
|
||||
Ok(handler)
|
||||
}
|
||||
|
||||
/// 处理rsync传输(参考rsync源码)
|
||||
pub fn handle_rsync(&mut self, channel: &mut dyn ReadWrite) -> Result<()> {
|
||||
info!("rsync handler: server={}, sender={}, root={}",
|
||||
self.server_mode, self.sender_mode, self.root_dir.display()); // 使用display()(Rust标准)
|
||||
|
||||
if !self.server_mode {
|
||||
return Err(anyhow!("rsync --server mode required"));
|
||||
}
|
||||
|
||||
// rsync协议版本协商
|
||||
self.negotiate_protocol(channel)?;
|
||||
|
||||
if self.sender_mode {
|
||||
// rsync --server --sender模式(发送文件列表)
|
||||
self.handle_sender_mode(channel)?;
|
||||
pub fn feed(&mut self, data: &[u8]) -> Result<()> {
|
||||
if self.multiplex {
|
||||
self.raw_input.extend_from_slice(data);
|
||||
self.decode_multiplex();
|
||||
} else {
|
||||
// rsync --server模式(接收文件)
|
||||
self.handle_receiver_mode(channel)?;
|
||||
self.rsync_input.extend_from_slice(data);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
self.process()
|
||||
}
|
||||
|
||||
/// rsync协议版本协商(参考rsync源码)
|
||||
fn negotiate_protocol(&mut self, channel: &mut dyn ReadWrite) -> Result<()> {
|
||||
debug!("rsync protocol negotiation");
|
||||
|
||||
// rsync协议握手:@RSYNCD: 30
|
||||
let handshake = "@RSYNCD: 30\n";
|
||||
channel.write_all(handshake.as_bytes())?;
|
||||
channel.flush()?;
|
||||
|
||||
// 读取客户端协议版本
|
||||
let mut response = String::new();
|
||||
let mut reader = BufReader::new(channel);
|
||||
reader.read_line(&mut response)?;
|
||||
|
||||
if !response.starts_with("@RSYNCD: ") {
|
||||
return Err(anyhow!("Invalid rsync handshake: {}", response));
|
||||
}
|
||||
|
||||
let client_version: u32 = response.trim_start_matches("@RSYNCD: ")
|
||||
.trim()
|
||||
.parse()?;
|
||||
|
||||
info!("rsync client version: {}", client_version);
|
||||
|
||||
// 选择最低版本
|
||||
self.protocol_version = std::cmp::min(client_version, 30);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// rsync --server --sender模式(发送文件列表)
|
||||
fn handle_sender_mode(&self, channel: &mut dyn ReadWrite) -> Result<()> {
|
||||
info!("rsync sender mode: sending file list");
|
||||
|
||||
// 发送模块列表(简化:仅发送root_dir)
|
||||
let module_list = format!("{}\n", self.root_dir.display());
|
||||
channel.write_all(module_list.as_bytes())?;
|
||||
channel.flush()?;
|
||||
|
||||
// 等待客户端选择模块
|
||||
let mut response = String::new();
|
||||
let mut reader = BufReader::new(&mut *channel); // 重新借用(Rust标准)
|
||||
reader.read_line(&mut response)?;
|
||||
|
||||
debug!("rsync module selected: {}", response.trim());
|
||||
|
||||
// 发送文件列表
|
||||
self.send_file_list(channel)?;
|
||||
|
||||
// 发送文件内容(简化:完整传输,不实现增量传输)
|
||||
self.send_files(channel)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// rsync --server模式(接收文件)
|
||||
fn handle_receiver_mode(&mut self, channel: &mut dyn ReadWrite) -> Result<()> {
|
||||
info!("rsync receiver mode: receiving files");
|
||||
|
||||
// 接收模块列表请求
|
||||
let mut response = String::new();
|
||||
let mut reader = BufReader::new(&mut *channel); // 重新借用(Rust标准)
|
||||
reader.read_line(&mut response)?;
|
||||
|
||||
debug!("rsync module request: {}", response.trim());
|
||||
|
||||
// 发送模块列表
|
||||
let module_list = format!("{}\n", self.root_dir.display());
|
||||
channel.write_all(module_list.as_bytes())?;
|
||||
channel.flush()?;
|
||||
|
||||
// 接收文件列表
|
||||
self.receive_file_list(channel)?;
|
||||
|
||||
// 接收文件内容
|
||||
self.receive_files(channel)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 发送文件列表(参考rsync源码)
|
||||
fn send_file_list(&self, channel: &mut dyn ReadWrite) -> Result<()> {
|
||||
debug!("rsync sending file list");
|
||||
|
||||
let full_path = self.resolve_path(&self.root_dir.to_string_lossy())?;
|
||||
|
||||
if full_path.is_file() {
|
||||
self.send_file_entry(channel, &full_path)?;
|
||||
} else if full_path.is_dir() {
|
||||
for entry in fs::read_dir(&full_path)? {
|
||||
let entry = entry?;
|
||||
self.send_file_entry(channel, &entry.path())?;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送文件列表结束标记
|
||||
channel.write_all(&[0])?;
|
||||
channel.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 发送文件条目(参考rsync源码)
|
||||
fn send_file_entry(&self, channel: &mut dyn ReadWrite, path: &Path) -> Result<()> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
let size = metadata.len();
|
||||
let mode = metadata.permissions().mode();
|
||||
let filename = path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
// rsync文件条目格式:mode size filename
|
||||
// 简化实现:仅发送基本信息
|
||||
let entry = format!("{} {} {}\n", mode, size, filename);
|
||||
channel.write_all(entry.as_bytes())?;
|
||||
|
||||
debug!("rsync file entry: {} ({} bytes)", filename, size);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 接收文件列表(参考rsync源码)
|
||||
fn receive_file_list(&self, channel: &mut dyn ReadWrite) -> Result<()> {
|
||||
debug!("rsync receiving file list");
|
||||
|
||||
let mut reader = BufReader::new(channel);
|
||||
let mut line = String::new();
|
||||
|
||||
while reader.read_line(&mut line)? > 0 {
|
||||
if line.trim().is_empty() {
|
||||
break; // 文件列表结束
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.trim().split_whitespace().collect();
|
||||
if parts.len() >= 3 {
|
||||
let mode: u32 = parts[0].parse()?;
|
||||
let size: u64 = parts[1].parse()?;
|
||||
let filename = parts[2];
|
||||
|
||||
debug!("rsync file entry received: {} ({} bytes)", filename, size);
|
||||
}
|
||||
|
||||
line.clear();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 发送文件(参考rsync源码)
|
||||
fn send_files(&self, channel: &mut dyn ReadWrite) -> Result<()> {
|
||||
info!("rsync sending files");
|
||||
|
||||
let full_path = self.resolve_path(&self.root_dir.to_string_lossy())?;
|
||||
|
||||
if full_path.is_file() {
|
||||
self.send_file_content(channel, &full_path)?;
|
||||
} else if full_path.is_dir() {
|
||||
for entry in fs::read_dir(&full_path)? {
|
||||
let entry = entry?;
|
||||
if entry.path().is_file() {
|
||||
self.send_file_content(channel, &entry.path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送结束标记
|
||||
channel.write_all(&[0])?;
|
||||
channel.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 发送文件内容(参考rsync源码)
|
||||
fn send_file_content(&self, channel: &mut dyn ReadWrite, path: &Path) -> Result<()> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
let size = metadata.len();
|
||||
let filename = path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
debug!("rsync sending file content: {} ({} bytes)", filename, size);
|
||||
|
||||
// rsync文件内容格式:size data checksum
|
||||
// 简化实现:发送文件大小 + 文件内容
|
||||
let size_bytes = size.to_be_bytes();
|
||||
channel.write_all(&size_bytes)?;
|
||||
|
||||
// 发送文件内容
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut buffer = vec![0u8; 8192];
|
||||
|
||||
while let Ok(n) = reader.read(&mut buffer) {
|
||||
if n == 0 {
|
||||
/// Strip multiplex headers from raw_input → rsync_input
|
||||
fn decode_multiplex(&mut self) {
|
||||
loop {
|
||||
if self.raw_input.len() < 4 {
|
||||
break;
|
||||
}
|
||||
let header = u32::from_le_bytes([
|
||||
self.raw_input[0], self.raw_input[1],
|
||||
self.raw_input[2], self.raw_input[3],
|
||||
]);
|
||||
let raw_tag = ((header >> 24) & 0xFF) as u8;
|
||||
let tag = raw_tag.wrapping_sub(MPLEX_BASE as u8);
|
||||
let len = (header & 0x00FF_FFFF) as usize;
|
||||
let total = 4 + len;
|
||||
if self.raw_input.len() < total {
|
||||
break;
|
||||
}
|
||||
channel.write_all(&buffer[..n])?;
|
||||
}
|
||||
|
||||
channel.flush()?;
|
||||
let payload = self.raw_input[4..total].to_vec();
|
||||
self.raw_input.drain(..total);
|
||||
|
||||
info!("rsync file sent: {} ({} bytes)", filename, size);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 接收文件(参考rsync源码)
|
||||
fn receive_files(&self, channel: &mut dyn ReadWrite) -> Result<()> {
|
||||
info!("rsync receiving files");
|
||||
|
||||
let mut reader = BufReader::new(channel);
|
||||
|
||||
while true {
|
||||
// 读取文件大小(8字节)
|
||||
let mut size_bytes = [0u8; 8];
|
||||
match reader.read_exact(&mut size_bytes) {
|
||||
Ok(_) => {
|
||||
let size = u64::from_be_bytes(size_bytes);
|
||||
|
||||
if size == 0 {
|
||||
break; // 结束标记
|
||||
}
|
||||
|
||||
// 简化:使用默认文件名
|
||||
let filename = "received_file.txt";
|
||||
let full_path = self.resolve_path(filename)?;
|
||||
|
||||
// 接收文件内容
|
||||
let file = File::create(&full_path)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
let mut buffer = vec![0u8; 8192];
|
||||
let mut remaining = size;
|
||||
|
||||
while remaining > 0 {
|
||||
let to_read = std::cmp::min(buffer.len() as u64, remaining) as usize;
|
||||
let n = reader.read(&mut buffer[..to_read])?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
writer.write_all(&buffer[..n])?;
|
||||
remaining -= n as u64;
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
|
||||
info!("rsync file received: {} ({} bytes)", filename, size);
|
||||
match tag {
|
||||
MSG_DATA => {
|
||||
self.rsync_input.extend_from_slice(&payload);
|
||||
}
|
||||
MSG_DONE => {
|
||||
info!("rsync: MSG_DONE received (file complete)");
|
||||
// Signal file completion by appending a sentinel to rsync_input
|
||||
self.rsync_input.extend_from_slice(b"RSYNCDONE");
|
||||
}
|
||||
9 => {
|
||||
warn!("rsync: MSG_REDO not handled");
|
||||
}
|
||||
_ => {
|
||||
debug!("rsync: unknown multiplex tag {} len={}", tag, len);
|
||||
}
|
||||
Err(_) => break, // EOF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drain_output(&mut self) -> Vec<u8> {
|
||||
let data = std::mem::take(&mut self.output_raw);
|
||||
if data.is_empty() || !self.multiplex {
|
||||
return data;
|
||||
}
|
||||
// Wrap with multiplex header (MSG_DATA)
|
||||
let header = (MPLEX_BASE << 24) | (data.len() as u32);
|
||||
let mut wrapped = Vec::with_capacity(4 + data.len());
|
||||
wrapped.extend_from_slice(&header.to_le_bytes());
|
||||
wrapped.extend_from_slice(&data);
|
||||
wrapped
|
||||
}
|
||||
|
||||
pub fn pending_output_len(&self) -> usize {
|
||||
self.output_raw.len()
|
||||
}
|
||||
|
||||
pub fn has_pending_output(&self) -> bool {
|
||||
!self.output_raw.is_empty()
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.state == RsyncState::Done
|
||||
}
|
||||
|
||||
pub fn total_received(&self) -> u64 {
|
||||
self.total_written
|
||||
}
|
||||
|
||||
fn transition(&mut self, new_state: RsyncState) {
|
||||
let old = std::mem::replace(&mut self.state, new_state.clone());
|
||||
debug!("RsyncHandler: {:?} -> {:?}", old, new_state);
|
||||
}
|
||||
|
||||
fn process(&mut self) -> Result<()> {
|
||||
loop {
|
||||
match self.state.clone() {
|
||||
RsyncState::SendVersion => {
|
||||
// Version already sent in constructor
|
||||
self.transition(RsyncState::WaitVersion);
|
||||
}
|
||||
|
||||
RsyncState::WaitVersion => {
|
||||
if self.rsync_input.len() >= 4 {
|
||||
let version = u32::from_le_bytes([
|
||||
self.rsync_input[0], self.rsync_input[1],
|
||||
self.rsync_input[2], self.rsync_input[3],
|
||||
]);
|
||||
self.rsync_input.drain(..4);
|
||||
self.protocol_version = std::cmp::min(self.protocol_version, version);
|
||||
info!("rsync: negotiated protocol version {}", self.protocol_version);
|
||||
self.multiplex = self.protocol_version >= 30;
|
||||
self.transition(RsyncState::ReadFileList);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
RsyncState::ReadFileList => {
|
||||
loop {
|
||||
if self.rsync_input.is_empty() { break; }
|
||||
|
||||
let flags = self.rsync_input[0];
|
||||
if flags == 0 {
|
||||
// End of file list
|
||||
self.rsync_input.drain(..1);
|
||||
info!("rsync: file list end ({} entries)", self.file_entries.len());
|
||||
|
||||
if self.file_entries.is_empty() {
|
||||
self.file_entries.push("file".to_string());
|
||||
}
|
||||
self.current_file = 0;
|
||||
// Enter sum head reading state
|
||||
self.transition(RsyncState::ReadSumHead { need: 20 });
|
||||
break;
|
||||
}
|
||||
|
||||
let mut pos = 1;
|
||||
|
||||
// Extended flags
|
||||
let _more_flags = if flags & 0x80 != 0 {
|
||||
if self.rsync_input.len() <= pos { break; }
|
||||
let ef = self.rsync_input[pos];
|
||||
pos += 1;
|
||||
ef
|
||||
} else { 0 };
|
||||
|
||||
let has_name = !(flags & 0x02 != 0 && self.current_file > 0);
|
||||
|
||||
if has_name {
|
||||
if let Some(nul_pos) = self.rsync_input[pos..].iter().position(|&b| b == 0) {
|
||||
let name = String::from_utf8_lossy(&self.rsync_input[pos..pos + nul_pos]).to_string();
|
||||
pos += nul_pos + 1;
|
||||
self.file_entries.push(name.clone());
|
||||
debug!("rsync: file entry: {}", name);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
let name = if !self.file_entries.is_empty() {
|
||||
self.file_entries[self.current_file].clone()
|
||||
} else {
|
||||
"file".to_string()
|
||||
};
|
||||
self.file_entries.push(name);
|
||||
}
|
||||
|
||||
// Skip metadata varints
|
||||
let skip_count = if flags & 0x10 == 0 { 1 } else { 0 }
|
||||
+ if flags & 0x20 == 0 { 1 } else { 0 }
|
||||
+ if flags & 0x40 == 0 { 1 } else { 0 }
|
||||
+ if flags & 0x08 == 0 { 1 } else { 0 }
|
||||
+ 1
|
||||
+ if self.protocol_version >= 30 { 1 } else { 0 };
|
||||
|
||||
for _ in 0..skip_count {
|
||||
match read_varint(&self.rsync_input[pos..]) {
|
||||
Some((_, consumed)) => pos += consumed,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
if pos > self.rsync_input.len() {
|
||||
break;
|
||||
}
|
||||
self.current_file += 1;
|
||||
self.rsync_input.drain(..pos);
|
||||
}
|
||||
if self.state == RsyncState::ReadFileList {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
RsyncState::ReadSumHead { need } => {
|
||||
if self.rsync_input.len() >= need {
|
||||
// Read sum head: count, blength, s2length, remainder (4 × LE int)
|
||||
// + checksum seed (1 × LE int)
|
||||
// = 5 × 4 = 20 bytes
|
||||
let sum_count = i32::from_le_bytes([
|
||||
self.rsync_input[0], self.rsync_input[1],
|
||||
self.rsync_input[2], self.rsync_input[3],
|
||||
]);
|
||||
let _sum_blength = i32::from_le_bytes([
|
||||
self.rsync_input[4], self.rsync_input[5],
|
||||
self.rsync_input[6], self.rsync_input[7],
|
||||
]);
|
||||
let _sum_s2length = i32::from_le_bytes([
|
||||
self.rsync_input[8], self.rsync_input[9],
|
||||
self.rsync_input[10], self.rsync_input[11],
|
||||
]);
|
||||
let _sum_remainder = i32::from_le_bytes([
|
||||
self.rsync_input[12], self.rsync_input[13],
|
||||
self.rsync_input[14], self.rsync_input[15],
|
||||
]);
|
||||
let checksum_seed = i32::from_le_bytes([
|
||||
self.rsync_input[16], self.rsync_input[17],
|
||||
self.rsync_input[18], self.rsync_input[19],
|
||||
]);
|
||||
self.rsync_input.drain(..20);
|
||||
|
||||
info!("rsync: sum_head count={} seed={}", sum_count, checksum_seed);
|
||||
self.transition(RsyncState::SendSumCount);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
RsyncState::SendSumCount => {
|
||||
self.open_current_file()?;
|
||||
|
||||
// Send sum_count = 0 (4-byte LE int = we have no existing data)
|
||||
self.output_raw.extend_from_slice(&0u32.to_le_bytes());
|
||||
info!("rsync: sent sum_count=0, ready to receive file data");
|
||||
|
||||
self.transition(RsyncState::ReadFileData);
|
||||
}
|
||||
|
||||
RsyncState::ReadFileData => {
|
||||
// Data comes as raw bytes inside MSG_DATA multiplex packets.
|
||||
// MSG_DONE appends b"RSYNCDONE" to rsync_input.
|
||||
let done_marker = b"RSYNCDONE";
|
||||
if let Some(pos) = self.rsync_input.windows(done_marker.len())
|
||||
.position(|w| w == done_marker)
|
||||
{
|
||||
// Data before the marker
|
||||
if pos > 0 {
|
||||
let data = self.rsync_input[..pos].to_vec();
|
||||
self.rsync_input.drain(..pos);
|
||||
self.write_to_file(&data)?;
|
||||
}
|
||||
// Remove marker
|
||||
self.rsync_input.drain(..done_marker.len());
|
||||
|
||||
// Close file
|
||||
if let Some(mut file) = self.output_file.take() {
|
||||
if let Err(e) = file.flush() {
|
||||
warn!("rsync flush error: {}", e);
|
||||
}
|
||||
}
|
||||
info!("rsync: file {} complete ({} bytes written to {})",
|
||||
self.file_entries.get(self.current_file).unwrap_or(&"?".to_string()),
|
||||
self.total_written,
|
||||
self.dest_path.display(),
|
||||
);
|
||||
|
||||
self.current_file += 1;
|
||||
if self.current_file >= self.file_entries.len() {
|
||||
self.transition(RsyncState::Done);
|
||||
info!("rsync ALL DONE: {} bytes written to {}",
|
||||
self.total_written, self.dest_path.display());
|
||||
} else {
|
||||
// Next file sum head
|
||||
self.transition(RsyncState::ReadSumHead { need: 20 });
|
||||
}
|
||||
} else if !self.rsync_input.is_empty() {
|
||||
// Partial data, keep it in buffer for more
|
||||
let data = self.rsync_input.clone();
|
||||
self.rsync_input.clear();
|
||||
self.write_to_file(&data)?;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
RsyncState::Done => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 路径解析(安全性检查)
|
||||
fn resolve_path(&self, path: &str) -> Result<PathBuf> {
|
||||
let full_path = self.root_dir.join(path);
|
||||
|
||||
let canonical_path = full_path.canonicalize()
|
||||
.map_err(|e| anyhow!("Path resolution error: {}", e))?;
|
||||
|
||||
if !canonical_path.starts_with(&self.root_dir.canonicalize()?) {
|
||||
return Err(anyhow!("Path traversal attempt detected"));
|
||||
fn open_current_file(&mut self) -> Result<()> {
|
||||
if let Some(parent) = self.dest_path.parent() {
|
||||
fs::create_dir_all(parent).ok();
|
||||
}
|
||||
let file = File::create(&self.dest_path)?;
|
||||
self.output_file = Some(file);
|
||||
info!("rsync: opened {} for writing", self.dest_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(canonical_path)
|
||||
fn write_to_file(&mut self, data: &[u8]) -> Result<()> {
|
||||
if let Some(file) = &mut self.output_file {
|
||||
file.write_all(data)?;
|
||||
self.total_written += data.len() as u64;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Read + Write trait组合(用于Channel)
|
||||
pub trait ReadWrite: Read + Write {}
|
||||
impl<T: Read + Write> ReadWrite for T {}
|
||||
/// Read rsync varint (LSB-first 7-bit groups, 0xFF prefix for negative)
|
||||
fn read_varint(buf: &[u8]) -> Option<(i32, usize)> {
|
||||
if buf.is_empty() { return None; }
|
||||
|
||||
let mut pos = 0;
|
||||
let mut b = buf[pos];
|
||||
pos += 1;
|
||||
|
||||
let neg = if b == 0xFF {
|
||||
if pos >= buf.len() { return None; }
|
||||
b = buf[pos];
|
||||
pos += 1;
|
||||
true
|
||||
} else { false };
|
||||
|
||||
let mut x = (b & 0x7F) as i32;
|
||||
let mut shift = 7;
|
||||
|
||||
while b & 0x80 != 0 {
|
||||
if pos >= buf.len() { return None; }
|
||||
b = buf[pos];
|
||||
pos += 1;
|
||||
x |= ((b & 0x7F) as i32) << shift;
|
||||
shift += 7;
|
||||
}
|
||||
|
||||
if neg { Some((-x, pos)) } else { Some((x, pos)) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rsync_command_parse() {
|
||||
let handler = RsyncHandler::parse_rsync_command("rsync --server --sender .").unwrap();
|
||||
assert!(handler.server_mode);
|
||||
assert!(handler.sender_mode);
|
||||
fn test_parse_command() {
|
||||
let h = RsyncHandler::parse_rsync_command("rsync --server -g -l -o -p -D -r -t -v --dirs . /tmp/upload.bin").unwrap();
|
||||
assert_eq!(h.dest_path, PathBuf::from("/tmp/upload.bin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rsync_server_parse() {
|
||||
let handler = RsyncHandler::parse_rsync_command("rsync --server .").unwrap();
|
||||
assert!(handler.server_mode);
|
||||
assert!(!handler.sender_mode);
|
||||
fn test_parse_command_sender() {
|
||||
let h = RsyncHandler::parse_rsync_command("rsync --server --sender -vlogDtprz . /home/user/file.txt").unwrap();
|
||||
assert_eq!(h.dest_path, PathBuf::from("/home/user/file.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rsync_protocol_version() {
|
||||
let handler = RsyncHandler::new(PathBuf::from("/tmp"));
|
||||
assert_eq!(handler.protocol_version, 30);
|
||||
fn test_version_exchange() {
|
||||
let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/test.bin").unwrap();
|
||||
// Initial output: protocol version (30 as LE int)
|
||||
let output = h.drain_output();
|
||||
assert_eq!(output, b"\x1e\x00\x00\x00");
|
||||
assert_eq!(h.state, RsyncState::WaitVersion);
|
||||
|
||||
// Client sends its version (30 = 0x1E)
|
||||
h.feed(b"\x1e\x00\x00\x00").unwrap();
|
||||
assert_eq!(h.state, RsyncState::ReadFileList);
|
||||
assert!(h.multiplex);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_negotiate_down() {
|
||||
let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/test.bin").unwrap();
|
||||
let _ = h.drain_output();
|
||||
// Client has lower version (29)
|
||||
h.feed(b"\x1d\x00\x00\x00").unwrap();
|
||||
assert_eq!(h.protocol_version, 29);
|
||||
assert_eq!(h.state, RsyncState::ReadFileList);
|
||||
}
|
||||
|
||||
fn build_multiplex(data: &[u8]) -> Vec<u8> {
|
||||
let header = (MPLEX_BASE << 24) | (data.len() as u32);
|
||||
let mut buf = Vec::with_capacity(4 + data.len());
|
||||
buf.extend_from_slice(&header.to_le_bytes());
|
||||
buf.extend_from_slice(data);
|
||||
buf
|
||||
}
|
||||
|
||||
fn build_multiplex_done() -> Vec<u8> {
|
||||
let header = (MPLEX_BASE << 24) | 0u32; // MSG_DONE (tag=1 → raw_tag=8)
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&header.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_list_multiplex() {
|
||||
let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/rsync_test.bin").unwrap();
|
||||
let _ = h.drain_output();
|
||||
// Version exchange
|
||||
h.feed(b"\x1e\x00\x00\x00").unwrap();
|
||||
assert!(h.multiplex);
|
||||
|
||||
// Build file list with multiplex wrapping
|
||||
let mut flist = Vec::new();
|
||||
// Entry: flags=0, name="test.txt\0", + 6 varints
|
||||
flist.push(0);
|
||||
flist.extend_from_slice(b"test.txt");
|
||||
flist.push(0);
|
||||
|
||||
fn write_varint(buf: &mut Vec<u8>, val: i32) {
|
||||
if val == 0 { buf.push(0); return; }
|
||||
if val < 0 {
|
||||
buf.push(0xFF);
|
||||
let mut v = (-val) as u32;
|
||||
while v > 0 {
|
||||
let mut byte = (v & 0x7F) as u8;
|
||||
v >>= 7;
|
||||
if v > 0 { byte |= 0x80; }
|
||||
buf.push(byte);
|
||||
}
|
||||
} else {
|
||||
let mut v = val as u32;
|
||||
while v > 0 {
|
||||
let mut byte = (v & 0x7F) as u8;
|
||||
v >>= 7;
|
||||
if v > 0 { byte |= 0x80; }
|
||||
buf.push(byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
write_varint(&mut flist, 33188); // mode
|
||||
write_varint(&mut flist, 501); // uid
|
||||
write_varint(&mut flist, 20); // gid
|
||||
write_varint(&mut flist, 1700000000); // time
|
||||
write_varint(&mut flist, 100); // size
|
||||
write_varint(&mut flist, 0); // checksum seed
|
||||
// End marker
|
||||
flist.push(0);
|
||||
|
||||
// Sum head (5 ints = 20 bytes) as separate multiplex packet
|
||||
let mut sum_head = Vec::new();
|
||||
sum_head.extend_from_slice(&0i32.to_le_bytes()); // count
|
||||
sum_head.extend_from_slice(&7000i32.to_le_bytes()); // blength
|
||||
sum_head.extend_from_slice(&2i32.to_le_bytes()); // s2length
|
||||
sum_head.extend_from_slice(&100i32.to_le_bytes()); // remainder
|
||||
sum_head.extend_from_slice(&42i32.to_le_bytes()); // checksum_seed
|
||||
|
||||
// Feed file list
|
||||
h.feed(&build_multiplex(&flist)).unwrap();
|
||||
assert_eq!(h.state, RsyncState::ReadFileList); // Still reading, 0x00 end marker triggered transition
|
||||
assert_eq!(h.file_entries.len(), 1);
|
||||
|
||||
// Now feed sum head
|
||||
h.feed(&build_multiplex(&sum_head)).unwrap();
|
||||
assert_eq!(h.state, RsyncState::SendSumCount);
|
||||
|
||||
// Send sum count response
|
||||
let sum_resp = h.drain_output();
|
||||
assert_eq!(sum_resp.len(), 8); // 4-byte header + 4-byte int
|
||||
assert_eq!(&sum_resp[4..8], &0u32.to_le_bytes());
|
||||
assert_eq!(h.state, RsyncState::ReadFileData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_data_multiplex() {
|
||||
let mut h = RsyncHandler::parse_rsync_command("rsync --server . /tmp/rsync_test.bin").unwrap();
|
||||
let _ = h.drain_output();
|
||||
h.feed(b"\x1e\x00\x00\x00").unwrap(); // version
|
||||
|
||||
// Simple file list
|
||||
let mut flist = Vec::new();
|
||||
flist.push(0);
|
||||
flist.extend_from_slice(b"test.bin");
|
||||
flist.push(0);
|
||||
fn wv(buf: &mut Vec<u8>, val: i32) {
|
||||
if val == 0 { buf.push(0); return; }
|
||||
if val < 0 { buf.push(0xFF); let mut v = (-val) as u32; while v > 0 { let mut byte = (v & 0x7F) as u8; v >>= 7; if v > 0 { byte |= 0x80; } buf.push(byte); } }
|
||||
else { let mut v = val as u32; while v > 0 { let mut byte = (v & 0x7F) as u8; v >>= 7; if v > 0 { byte |= 0x80; } buf.push(byte); } }
|
||||
}
|
||||
wv(&mut flist, 33188); wv(&mut flist, 501); wv(&mut flist, 20);
|
||||
wv(&mut flist, 1700000000); wv(&mut flist, 100); wv(&mut flist, 0);
|
||||
flist.push(0);
|
||||
h.feed(&build_multiplex(&flist)).unwrap();
|
||||
|
||||
// Sum head
|
||||
let mut sh = Vec::new();
|
||||
sh.extend_from_slice(&0i32.to_le_bytes());
|
||||
sh.extend_from_slice(&7000i32.to_le_bytes());
|
||||
sh.extend_from_slice(&2i32.to_le_bytes());
|
||||
sh.extend_from_slice(&100i32.to_le_bytes());
|
||||
sh.extend_from_slice(&42i32.to_le_bytes());
|
||||
h.feed(&build_multiplex(&sh)).unwrap();
|
||||
let _ = h.drain_output(); // sum count response
|
||||
|
||||
// File data + MSG_DONE
|
||||
let file_data = b"Hello, rsync protocol!";
|
||||
h.feed(&build_multiplex(file_data)).unwrap();
|
||||
assert_eq!(h.state, RsyncState::ReadFileData);
|
||||
|
||||
// MSG_DONE
|
||||
// MSG_DONE has tag=1, so raw_tag = MPLEX_BASE + 1 = 8
|
||||
let done_header = (MPLEX_BASE + 1) << 24; // raw_tag = 8, len = 0
|
||||
let done_bytes = done_header.to_le_bytes();
|
||||
h.feed(&done_bytes).unwrap();
|
||||
|
||||
assert_eq!(h.state, RsyncState::Done);
|
||||
assert_eq!(h.total_written, file_data.len() as u64);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user