feat(security): [#1981] l-s v0.5.2 Unix 符号链接防护及安全文件操作
This commit is contained in:
+82
-21
@@ -5,13 +5,16 @@ mod meta;
|
||||
mod utils;
|
||||
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::Parser;
|
||||
use meta::{calc_xxh128_with_callback, scan_dir_xxh128, DirSnapshot, FileMeta, ProgressTracker};
|
||||
use meta::{
|
||||
calc_xxh128_from_file_with_callback, open_regular_file_nofollow, scan_dir_xxh128, DirSnapshot,
|
||||
FileMeta, ProgressTracker,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let started = Instant::now();
|
||||
@@ -19,10 +22,14 @@ fn main() -> Result<()> {
|
||||
let target = cli.resolve_path()?;
|
||||
println!("目标: {}", target.display());
|
||||
|
||||
if target.is_dir() {
|
||||
let target_info = fs::symlink_metadata(&target)
|
||||
.with_context(|| format!("无法读取路径信息: {}", target.display()))?;
|
||||
if target_info.is_dir() {
|
||||
process_dir(&target)?;
|
||||
} else {
|
||||
} else if target_info.is_file() {
|
||||
process_file(&target)?;
|
||||
} else {
|
||||
return Err(anyhow!("{} 不是文件或目录", target.display()));
|
||||
}
|
||||
|
||||
println!("耗时: {:?}", started.elapsed());
|
||||
@@ -35,8 +42,19 @@ fn process_file(path: &Path) -> Result<()> {
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("meta");
|
||||
fs::create_dir_all(&meta_dir)
|
||||
.with_context(|| format!("无法创建目录: {}", meta_dir.display()))?;
|
||||
match symlink_metadata_optional(&meta_dir)? {
|
||||
Some(info) if info.file_type().is_symlink() => {
|
||||
return Err(anyhow!("不支持符号链接目录: {}", meta_dir.display()));
|
||||
}
|
||||
Some(info) if !info.is_dir() => {
|
||||
return Err(anyhow!("{} 不是目录", meta_dir.display()));
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
fs::create_dir_all(&meta_dir)
|
||||
.with_context(|| format!("无法创建目录: {}", meta_dir.display()))?;
|
||||
}
|
||||
}
|
||||
|
||||
let basename = path
|
||||
.file_name()
|
||||
@@ -44,32 +62,45 @@ fn process_file(path: &Path) -> Result<()> {
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let save_path = meta_dir.join(format!("{basename}.json"));
|
||||
|
||||
// 获取文件大小
|
||||
let file_size = fs::metadata(path)
|
||||
.with_context(|| format!("无法读取文件信息: {}", path.display()))?
|
||||
.len();
|
||||
|
||||
if !save_path.exists() {
|
||||
let existing_save = symlink_metadata_optional(&save_path)?;
|
||||
if existing_save.is_none() {
|
||||
let file = open_regular_file_nofollow(path)?;
|
||||
let file_size = file
|
||||
.metadata()
|
||||
.with_context(|| format!("无法读取文件信息: {}", path.display()))?
|
||||
.len();
|
||||
let tracker = ProgressTracker::new_single_file(file_size, &basename);
|
||||
let on_bytes = tracker.bytes_callback();
|
||||
let on_iop = tracker.iop_callback();
|
||||
let meta = FileMeta::from_path_with_callback(path, on_bytes, on_iop)?;
|
||||
let meta = FileMeta::from_open_file_with_callback(path, file, on_bytes, on_iop)?;
|
||||
tracker.finish("处理完成");
|
||||
let json = meta.to_pretty_json()?;
|
||||
println!("{}", json);
|
||||
write_atomic(&save_path, &json)?;
|
||||
return Ok(());
|
||||
}
|
||||
let save_info = existing_save.expect("checked as Some");
|
||||
if save_info.file_type().is_symlink() {
|
||||
return Err(anyhow!("不支持符号链接元数据文件: {}", save_path.display()));
|
||||
}
|
||||
if !save_info.is_file() {
|
||||
return Err(anyhow!("{} 不是文件", save_path.display()));
|
||||
}
|
||||
|
||||
let existing = File::open(&save_path)
|
||||
let existing = open_regular_file_nofollow(&save_path)
|
||||
.with_context(|| format!("无法读取历史元数据: {}", save_path.display()))?;
|
||||
let old_meta = FileMeta::from_reader(existing)?;
|
||||
|
||||
// 使用进度条计算快速哈希
|
||||
let file = open_regular_file_nofollow(path)?;
|
||||
let file_size = file
|
||||
.metadata()
|
||||
.with_context(|| format!("无法读取文件信息: {}", path.display()))?
|
||||
.len();
|
||||
let tracker = ProgressTracker::new_single_file(file_size, &basename);
|
||||
let on_bytes = tracker.bytes_callback();
|
||||
let on_iop = tracker.iop_callback();
|
||||
let fast_hash = calc_xxh128_with_callback(path, on_bytes, on_iop)?;
|
||||
let fast_hash = calc_xxh128_from_file_with_callback(path, file, on_bytes, on_iop)?;
|
||||
tracker.finish("校验完成");
|
||||
|
||||
if fast_hash == old_meta.xxh128 {
|
||||
@@ -79,10 +110,15 @@ fn process_file(path: &Path) -> Result<()> {
|
||||
|
||||
println!("校验失败!");
|
||||
println!("现校验文件:");
|
||||
let file = open_regular_file_nofollow(path)?;
|
||||
let file_size = file
|
||||
.metadata()
|
||||
.with_context(|| format!("无法读取文件信息: {}", path.display()))?
|
||||
.len();
|
||||
let tracker = ProgressTracker::new_single_file(file_size, &basename);
|
||||
let on_bytes = tracker.bytes_callback();
|
||||
let on_iop = tracker.iop_callback();
|
||||
let meta = FileMeta::from_path_with_callback(path, on_bytes, on_iop)?;
|
||||
let meta = FileMeta::from_open_file_with_callback(path, file, on_bytes, on_iop)?;
|
||||
tracker.finish("处理完成");
|
||||
println!("{}", meta.to_pretty_json()?);
|
||||
println!("原校验文件:");
|
||||
@@ -95,23 +131,30 @@ fn process_dir(path: &Path) -> Result<()> {
|
||||
let meta_path = path.join("meta.json");
|
||||
let backup_path = path.join("meta-old.json");
|
||||
|
||||
if !meta_path.exists() {
|
||||
let Some(meta_info) = symlink_metadata_optional(&meta_path)? else {
|
||||
let snapshot = DirSnapshot::build_root(path)?;
|
||||
let json = serde_json::to_string_pretty(&snapshot)?;
|
||||
write_atomic(&meta_path, &json)?;
|
||||
return Ok(());
|
||||
};
|
||||
if meta_info.file_type().is_symlink() {
|
||||
return Err(anyhow!("不支持符号链接元数据文件: {}", meta_path.display()));
|
||||
}
|
||||
if !meta_info.is_file() {
|
||||
return Err(anyhow!("{} 不是文件", meta_path.display()));
|
||||
}
|
||||
|
||||
if backup_path.exists() {
|
||||
if symlink_metadata_optional(&backup_path)?.is_some() {
|
||||
fs::remove_file(&backup_path)?;
|
||||
}
|
||||
|
||||
fs::rename(&meta_path, &backup_path)
|
||||
.with_context(|| format!("无法重命名旧meta: {}", meta_path.display()))?;
|
||||
sync_parent_dir(&backup_path)?;
|
||||
println!("发现旧元数据,已暂存为 meta-old.json,开始校验...");
|
||||
|
||||
let meta_file =
|
||||
File::open(&backup_path).with_context(|| format!("无法读取: {}", backup_path.display()))?;
|
||||
let meta_file = open_regular_file_nofollow(&backup_path)
|
||||
.with_context(|| format!("无法读取: {}", backup_path.display()))?;
|
||||
let snapshot = DirSnapshot::from_reader(meta_file)?;
|
||||
let mut stored = snapshot.collect_file_map(path);
|
||||
let current = scan_dir_xxh128(path)?;
|
||||
@@ -145,6 +188,7 @@ fn process_dir(path: &Path) -> Result<()> {
|
||||
println!("校验通过.");
|
||||
fs::rename(&backup_path, &meta_path)
|
||||
.with_context(|| format!("无法恢复meta: {}", meta_path.display()))?;
|
||||
sync_parent_dir(&meta_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -180,6 +224,7 @@ fn write_atomic(path: &Path, contents: &str) -> Result<()> {
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
sync_parent_dir(path)?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
@@ -189,3 +234,19 @@ fn write_atomic(path: &Path, contents: &str) -> Result<()> {
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn symlink_metadata_optional(path: &Path) -> Result<Option<fs::Metadata>> {
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(info) => Ok(Some(info)),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err).with_context(|| format!("无法读取路径信息: {}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_parent_dir(path: &Path) -> Result<()> {
|
||||
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||
let dir = File::open(parent)
|
||||
.with_context(|| format!("无法打开父目录以同步: {}", parent.display()))?;
|
||||
dir.sync_all()
|
||||
.with_context(|| format!("无法同步父目录: {}", parent.display()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user