This commit is contained in:
mofeng-git
2025-12-28 18:19:16 +08:00
commit d143d158e4
771 changed files with 220548 additions and 0 deletions

16
libs/ventoy-img-rs/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# Build output
/target/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Test files
*.img
!resources/*.img

View File

@@ -0,0 +1,28 @@
[package]
name = "ventoy-img"
version = "0.1.0"
edition = "2021"
description = "Create and manage Ventoy bootable IMG files without root or loop devices"
license = "GPL-3.0"
[dependencies]
# CLI
clap = { version = "4", features = ["derive"] }
# Error handling
thiserror = "1"
# Time handling
chrono = "0.4"
# CRC32 for exFAT checksum
crc32fast = "1"
[dev-dependencies]
tempfile = "3"
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true

View File

@@ -0,0 +1,150 @@
# ventoy-img
纯 Rust 实现的 Ventoy 可启动镜像生成工具。无需 root 权限或 loop 设备即可创建完整可用的 Ventoy IMG 文件。
## 特性
- **纯 Rust 实现**: 无外部依赖,单一可执行文件
- **无需 root**: 不需要 loop 设备或管理员权限
- **内嵌资源**: 所有 Ventoy 启动文件内嵌于二进制中
- **完整 exFAT 支持**: 手写 exFAT 实现,支持大于 4GB 的 ISO 文件
- **流式读写**: 支持大文件流式读写,内存占用低
- **Unicode 支持**: 完整的 Unicode 文件名支持中日韩、西里尔、希腊字母、Emoji 等)
- **动态簇大小**: 根据卷大小自动选择最优簇大小4KB-128KB
- **跨平台**: 支持 Linux、macOS、Windows
## 快速开始
### 编译
```bash
cargo build --release
```
### 创建镜像
```bash
# 创建 8GB Ventoy 镜像
./target/release/ventoy-img create -s 8G -o ventoy.img
# 添加 ISO 文件
./target/release/ventoy-img add ventoy.img ubuntu.iso
./target/release/ventoy-img add ventoy.img windows.iso
# 列出文件
./target/release/ventoy-img list ventoy.img
# 写入 U 盘
sudo dd if=ventoy.img of=/dev/sdX bs=4M status=progress
```
## 命令
```
ventoy-img <COMMAND>
Commands:
create 创建新的 Ventoy IMG 文件
add 添加文件到镜像
list 列出镜像中的文件
remove 从镜像删除文件
info 显示镜像信息
```
### create
```bash
ventoy-img create [OPTIONS]
Options:
-s, --size <SIZE> 镜像大小 (如 8G, 16G, 1024M) [默认: 8G]
-o, --output <OUTPUT> 输出文件路径 [默认: ventoy.img]
-L, --label <LABEL> 数据分区卷标 [默认: Ventoy]
```
### add
```bash
ventoy-img add <IMAGE> <FILE>
```
### list
```bash
ventoy-img list <IMAGE>
```
### remove
```bash
ventoy-img remove <IMAGE> <NAME>
```
### info
```bash
ventoy-img info <IMAGE>
```
## 作为库使用
```rust
use ventoy_img::{VentoyImage, Result};
use std::path::Path;
fn main() -> Result<()> {
// 创建镜像
let mut img = VentoyImage::create(
Path::new("ventoy.img"),
"8G",
"Ventoy"
)?;
// 添加文件
img.add_file(Path::new("ubuntu.iso"))?;
// 列出文件
for file in img.list_files()? {
println!("{}: {} bytes", file.name, file.size);
}
Ok(())
}
```
## 文档
- [CLI 使用说明](docs/CLI.md) - 命令行工具详细用法
- [库使用说明](docs/LIBRARY.md) - Rust 库 API 参考
- [技术文档](docs/TECHNICAL.md) - 内部实现细节
## 镜像结构
```
┌────────────────────────────────────────────────────────────┐
│ MBR (512 bytes) - 引导代码 + 分区表 │
├────────────────────────────────────────────────────────────┤
│ GRUB core.img (Sector 1-2047) - BIOS 引导 │
├────────────────────────────────────────────────────────────┤
│ 数据分区 (exFAT) - 存放 ISO/IMG 文件 │
├────────────────────────────────────────────────────────────┤
│ EFI 分区 (FAT16, 32MB) - UEFI 引导 │
└────────────────────────────────────────────────────────────┘
```
## 依赖
- `clap` - 命令行解析
- `thiserror` - 错误处理
- `lzma-rs` - XZ 解压缩
- `chrono` - 时间处理
- `crc32fast` - CRC32 校验
## 许可证
GPL-3.0
## 致谢
- [Ventoy](https://www.ventoy.net/) - 原始项目
- [GRUB](https://www.gnu.org/software/grub/) - 引导加载器

View File

@@ -0,0 +1,531 @@
# ventoy-img CLI 使用说明
## 安装
### 从源码编译
```bash
# 克隆仓库
git clone https://github.com/user/ventoy-img-rs.git
cd ventoy-img-rs
# 编译 release 版本
cargo build --release
# 二进制文件位于 target/release/ventoy-img
```
### 直接使用
```bash
# 复制到 PATH
sudo cp target/release/ventoy-img /usr/local/bin/
# 或添加别名
alias ventoy-img='/path/to/ventoy-img-rs/target/release/ventoy-img'
```
## 命令概览
```
ventoy-img <COMMAND>
Commands:
create 创建新的 Ventoy IMG 文件
add 添加文件到镜像(支持子目录和覆盖)
list 列出镜像中的文件(支持递归列出)
remove 从镜像删除文件或目录(支持递归删除)
mkdir 创建目录(支持递归创建父目录)
info 显示镜像信息
help 显示帮助信息
```
## 命令详解
### create - 创建镜像
创建新的 Ventoy 可启动 IMG 文件。
```bash
ventoy-img create [OPTIONS]
```
**选项:**
| 选项 | 简写 | 默认值 | 说明 |
|------|------|--------|------|
| `--size` | `-s` | `8G` | 镜像大小 |
| `--output` | `-o` | `ventoy.img` | 输出文件路径 |
| `--label` | `-L` | `Ventoy` | 数据分区卷标 |
**大小格式:**
- `G``GB`: 千兆字节,如 `8G`, `16G`
- `M``MB`: 兆字节,如 `512M`, `1024M`
- 纯数字: 字节数,如 `8589934592`
**示例:**
```bash
# 创建 8GB 镜像(默认)
ventoy-img create
# 创建 16GB 镜像,指定输出路径
ventoy-img create -s 16G -o /path/to/my-ventoy.img
# 创建 512MB 小镜像,自定义卷标
ventoy-img create -s 512M -o small.img -L "MyUSB"
# 创建 32GB 镜像
ventoy-img create --size 32G --output ventoy-32g.img --label "Ventoy32"
```
**输出示例:**
```
========================================
Ventoy IMG Creator (Rust Edition)
========================================
[INFO] Creating 8192MB image: ventoy.img
[INFO] Writing boot code...
[INFO] Writing MBR partition table...
Data partition: sector 2048 - 16744447 (8160 MB)
EFI partition: sector 16744448 - 16809983 (32 MB)
[INFO] Writing Ventoy signature...
[INFO] Writing EFI partition...
[INFO] Formatting data partition as exFAT...
[INFO] Ventoy IMG created successfully!
========================================
Image: ventoy.img
Size: 8G
Label: Ventoy
========================================
```
### add - 添加文件
将 ISO/IMG 文件添加到 Ventoy 镜像的数据分区。
```bash
ventoy-img add [OPTIONS] <IMAGE> <FILE>
```
**参数:**
- `IMAGE`: Ventoy IMG 文件路径
- `FILE`: 要添加的文件路径
**选项:**
| 选项 | 简写 | 说明 |
|------|------|------|
| `--dest` | `-d` | 目标路径(支持子目录,如 `iso/linux/ubuntu.iso` |
| `--force` | `-f` | 覆盖已存在的文件 |
| `--parents` | `-p` | 自动创建父目录 |
**示例:**
```bash
# 添加单个 ISO 到根目录
ventoy-img add ventoy.img ubuntu-22.04-desktop-amd64.iso
# 添加到子目录(目录必须存在)
ventoy-img add ventoy.img ubuntu.iso -d iso/linux/ubuntu.iso
# 添加到子目录并自动创建父目录
ventoy-img add ventoy.img ubuntu.iso -d iso/linux/ubuntu.iso -p
# 覆盖已存在的文件
ventoy-img add ventoy.img new-ubuntu.iso -d iso/linux/ubuntu.iso -f
# 组合使用:创建目录 + 覆盖
ventoy-img add ventoy.img ubuntu.iso -d iso/linux/ubuntu.iso -p -f
```
**批量添加(使用 shell**
```bash
# 添加目录下所有 ISO 到根目录
for iso in /path/to/isos/*.iso; do
ventoy-img add ventoy.img "$iso"
done
# 添加到子目录并保持目录结构
for iso in /path/to/isos/*.iso; do
ventoy-img add ventoy.img "$iso" -d "iso/$(basename "$iso")" -p
done
```
### list - 列出文件
列出镜像数据分区中的文件。
```bash
ventoy-img list [OPTIONS] <IMAGE>
```
**选项:**
| 选项 | 简写 | 说明 |
|------|------|------|
| `--path` | | 指定要列出的目录路径 |
| `--recursive` | `-r` | 递归列出所有文件和目录 |
**示例:**
```bash
# 列出根目录
ventoy-img list ventoy.img
# 列出指定目录
ventoy-img list ventoy.img --path iso/linux
# 递归列出所有文件
ventoy-img list ventoy.img -r
```
**输出示例(根目录):**
```
NAME SIZE TYPE
------------------------------------------------------------
ubuntu-22.04-desktop-amd64.iso 3.6 GB FILE
iso 0 B DIR
```
**输出示例(递归):**
```
PATH SIZE TYPE
----------------------------------------------------------------------
iso 0 B DIR
iso/linux 0 B DIR
iso/linux/ubuntu.iso 3.6 GB FILE
iso/windows 0 B DIR
iso/windows/win11.iso 5.2 GB FILE
```
**空镜像输出:**
```
No files in image
```
### remove - 删除文件或目录
从镜像中删除指定文件或目录。
```bash
ventoy-img remove [OPTIONS] <IMAGE> <PATH>
```
**参数:**
- `IMAGE`: Ventoy IMG 文件路径
- `PATH`: 要删除的文件或目录路径
**选项:**
| 选项 | 简写 | 说明 |
|------|------|------|
| `--recursive` | `-r` | 递归删除目录及其内容 |
**示例:**
```bash
# 删除根目录的文件
ventoy-img remove ventoy.img ubuntu.iso
# 删除子目录中的文件
ventoy-img remove ventoy.img iso/linux/ubuntu.iso
# 删除空目录
ventoy-img remove ventoy.img iso/empty-dir
# 递归删除目录及其所有内容
ventoy-img remove ventoy.img iso -r
# 文件名大小写不敏感
ventoy-img remove ventoy.img ISO/LINUX/UBUNTU.ISO
```
**注意:**
- 删除非空目录时必须使用 `-r` 选项
- 递归删除会删除目录下的所有文件和子目录
### mkdir - 创建目录
在镜像中创建目录。
```bash
ventoy-img mkdir [OPTIONS] <IMAGE> <PATH>
```
**参数:**
- `IMAGE`: Ventoy IMG 文件路径
- `PATH`: 要创建的目录路径
**选项:**
| 选项 | 简写 | 说明 |
|------|------|------|
| `--parents` | `-p` | 递归创建父目录(类似 `mkdir -p` |
**示例:**
```bash
# 创建单级目录
ventoy-img mkdir ventoy.img iso
# 递归创建多级目录
ventoy-img mkdir ventoy.img iso/linux/ubuntu -p
# 创建多个目录
ventoy-img mkdir ventoy.img iso -p
ventoy-img mkdir ventoy.img iso/linux -p
ventoy-img mkdir ventoy.img iso/windows -p
```
### info - 显示信息
显示镜像的详细信息。
```bash
ventoy-img info <IMAGE>
```
**示例:**
```bash
ventoy-img info ventoy.img
```
**输出示例:**
```
Image: ventoy.img
Partition Layout:
Data partition:
Start: sector 2048 (offset 1.0 MB)
Size: 16742400 sectors (8.0 GB)
EFI partition:
Start: sector 16744448 (offset 8.0 GB)
Size: 65536 sectors (32 MB)
```
## 使用场景
### 场景 1: 创建多系统启动盘
```bash
# 1. 创建 32GB 镜像
ventoy-img create -s 32G -o multiboot.img
# 2. 添加各种系统 ISO
ventoy-img add multiboot.img ubuntu-22.04.iso
ventoy-img add multiboot.img windows11.iso
ventoy-img add multiboot.img fedora-39.iso
ventoy-img add multiboot.img archlinux.iso
# 3. 查看文件列表
ventoy-img list multiboot.img
# 4. 写入 U 盘
sudo dd if=multiboot.img of=/dev/sdX bs=4M status=progress
```
### 场景 2: 维护现有镜像
```bash
# 查看当前文件(递归)
ventoy-img list ventoy.img -r
# 删除旧版本
ventoy-img remove ventoy.img iso/linux/ubuntu-20.04.iso
# 添加新版本(覆盖)
ventoy-img add ventoy.img ubuntu-24.04.iso -d iso/linux/ubuntu-24.04.iso -f
# 确认更改
ventoy-img list ventoy.img -r
```
### 场景 2.5: 组织文件到子目录
```bash
# 创建目录结构
ventoy-img mkdir ventoy.img iso/linux -p
ventoy-img mkdir ventoy.img iso/windows -p
ventoy-img mkdir ventoy.img iso/tools -p
# 添加文件到对应目录
ventoy-img add ventoy.img ubuntu.iso -d iso/linux/ubuntu.iso
ventoy-img add ventoy.img fedora.iso -d iso/linux/fedora.iso
ventoy-img add ventoy.img win11.iso -d iso/windows/win11.iso
ventoy-img add ventoy.img hiren.iso -d iso/tools/hiren.iso
# 查看目录结构
ventoy-img list ventoy.img -r
```
### 场景 3: 自动化脚本
```bash
#!/bin/bash
# create-ventoy.sh - 自动创建 Ventoy 镜像
ISO_DIR="/path/to/isos"
OUTPUT="ventoy-$(date +%Y%m%d).img"
SIZE="64G"
# 创建镜像
ventoy-img create -s "$SIZE" -o "$OUTPUT" || exit 1
# 添加所有 ISO
for iso in "$ISO_DIR"/*.iso; do
if [ -f "$iso" ]; then
echo "Adding: $(basename "$iso")"
ventoy-img add "$OUTPUT" "$iso" || echo "Failed: $iso"
fi
done
# 显示结果
echo ""
echo "=== Created: $OUTPUT ==="
ventoy-img list "$OUTPUT"
```
### 场景 4: 在没有 root 权限的环境中使用
```bash
# 在用户目录创建镜像
ventoy-img create -s 8G -o ~/ventoy.img
# 添加文件
ventoy-img add ~/ventoy.img ~/Downloads/linux.iso
# 之后可以用 dd 写入 U 盘(需要 root
# 或者复制到有权限的机器上写入
```
## 写入 U 盘
创建的 IMG 文件可以直接写入 U 盘:
### Linux
```bash
# 查找 U 盘设备
lsblk
# 写入(替换 sdX 为实际设备)
sudo dd if=ventoy.img of=/dev/sdX bs=4M status=progress conv=fsync
# 或使用 pv 显示进度
pv ventoy.img | sudo dd of=/dev/sdX bs=4M conv=fsync
```
### macOS
```bash
# 查找 U 盘
diskutil list
# 卸载
diskutil unmountDisk /dev/diskN
# 写入
sudo dd if=ventoy.img of=/dev/rdiskN bs=4m
# 弹出
diskutil eject /dev/diskN
```
### Windows
使用 [Rufus](https://rufus.ie/) 或 [balenaEtcher](https://www.balena.io/etcher/)
1. 选择 ventoy.img 文件
2. 选择目标 U 盘
3. 点击写入
## 常见问题
### Q: 镜像最小可以多大?
A: 最小 64MB32MB EFI 分区 + 32MB 数据分区)。但实际使用建议至少 512MB。
### Q: 支持多大的 ISO 文件?
A: 理论上支持 exFAT 的最大文件大小(约 16 EB。实际受限于镜像大小和可用空间。
### Q: 为什么添加文件失败?
可能原因:
1. 镜像空间不足
2. 文件名已存在(使用 `-f` 选项覆盖,或先删除)
3. 目标目录不存在(使用 `-p` 选项自动创建)
4. 文件名包含非法字符
5. 镜像文件损坏
### Q: 如何覆盖已存在的文件?
使用 `-f``--force` 选项:
```bash
ventoy-img add ventoy.img new-file.iso -d existing-file.iso -f
```
### Q: 如何创建多级目录?
使用 `-p``--parents` 选项:
```bash
# 创建目录
ventoy-img mkdir ventoy.img path/to/deep/dir -p
# 或在添加文件时自动创建
ventoy-img add ventoy.img file.iso -d path/to/deep/file.iso -p
```
### Q: 如何删除整个目录?
使用 `-r``--recursive` 选项:
```bash
ventoy-img remove ventoy.img directory-name -r
```
### Q: 如何验证镜像是否正确?
```bash
# 检查分区表
fdisk -l ventoy.img
# 检查 Ventoy 签名
xxd -s 0x190 -l 16 ventoy.img
# 应显示: 5654 0047 6500 4844 0052 6400 2045 720d
# 列出文件
ventoy-img list ventoy.img
```
### Q: 可以在 Windows 上使用吗?
A: 可以。编译 Windows 版本:
```bash
cargo build --release --target x86_64-pc-windows-gnu
```
## 退出码
| 码 | 含义 |
|----|------|
| 0 | 成功 |
| 1 | 错误(详见错误信息) |
## 环境变量
目前不使用任何环境变量。
## 另请参阅
- [技术文档](TECHNICAL.md) - 内部实现细节
- [库使用说明](LIBRARY.md) - Rust 库 API
- [Ventoy 官方文档](https://www.ventoy.net/en/doc_start.html)

View File

@@ -0,0 +1,579 @@
# ventoy-img 库使用说明
## 安装
### 作为依赖添加
```toml
[dependencies]
ventoy-img = { path = "path/to/ventoy-img-rs" }
```
或发布到 crates.io 后:
```toml
[dependencies]
ventoy-img = "0.1"
```
## 快速开始
```rust
use ventoy_img::{VentoyImage, Result};
use std::path::Path;
fn main() -> Result<()> {
// 创建 8GB Ventoy 镜像
let img = VentoyImage::create(
Path::new("ventoy.img"),
"8G",
"Ventoy"
)?;
// 打开已有镜像
let mut img = VentoyImage::open(Path::new("ventoy.img"))?;
// 列出文件
for file in img.list_files()? {
println!("{}: {} bytes", file.name, file.size);
}
Ok(())
}
```
## API 参考
### VentoyImage
主要的镜像操作结构体。
#### 创建镜像
```rust
pub fn create(path: &Path, size_str: &str, label: &str) -> Result<Self>
```
创建新的 Ventoy IMG 文件。
**参数:**
- `path`: 输出文件路径
- `size_str`: 镜像大小,支持格式:`"8G"`, `"16G"`, `"1024M"`, `"1073741824"`
- `label`: 数据分区卷标(最长 11 字符)
**示例:**
```rust
// 创建 8GB 镜像
let img = VentoyImage::create(Path::new("ventoy.img"), "8G", "Ventoy")?;
// 创建 512MB 镜像
let img = VentoyImage::create(Path::new("small.img"), "512M", "MyUSB")?;
```
#### 打开镜像
```rust
pub fn open(path: &Path) -> Result<Self>
```
打开已有的 Ventoy IMG 文件。会验证 Ventoy 签名。
**示例:**
```rust
let img = VentoyImage::open(Path::new("ventoy.img"))?;
```
#### 列出文件
```rust
pub fn list_files(&self) -> Result<Vec<FileInfo>>
pub fn list_files_at(&self, path: &str) -> Result<Vec<FileInfo>>
pub fn list_files_recursive(&self) -> Result<Vec<FileInfo>>
```
列出数据分区中的文件。
**返回:**
```rust
pub struct FileInfo {
pub name: String,
pub size: u64,
pub is_directory: bool,
pub path: String, // 完整路径(用于递归列出)
}
```
**示例:**
```rust
let img = VentoyImage::open(Path::new("ventoy.img"))?;
// 列出根目录
for file in img.list_files()? {
if file.is_directory {
println!("[DIR] {}", file.name);
} else {
println!("[FILE] {} ({} bytes)", file.name, file.size);
}
}
// 列出指定目录
for file in img.list_files_at("iso/linux")? {
println!("{}", file.name);
}
// 递归列出所有文件
for file in img.list_files_recursive()? {
println!("{}: {} bytes", file.path, file.size);
}
```
#### 添加文件
```rust
pub fn add_file(&mut self, src_path: &Path) -> Result<()>
pub fn add_file_overwrite(&mut self, src_path: &Path, overwrite: bool) -> Result<()>
pub fn add_file_to_path(&mut self, src_path: &Path, dest_path: &str, create_parents: bool, overwrite: bool) -> Result<()>
```
将文件添加到数据分区。
**示例:**
```rust
let mut img = VentoyImage::open(Path::new("ventoy.img"))?;
// 添加到根目录
img.add_file(Path::new("/path/to/ubuntu.iso"))?;
// 添加并覆盖已存在的文件
img.add_file_overwrite(Path::new("/path/to/new-ubuntu.iso"), true)?;
// 添加到子目录(自动创建父目录)
img.add_file_to_path(
Path::new("/path/to/ubuntu.iso"),
"iso/linux/ubuntu.iso",
true, // create_parents
false, // overwrite
)?;
// 添加并覆盖子目录中的文件
img.add_file_to_path(
Path::new("/path/to/new-ubuntu.iso"),
"iso/linux/ubuntu.iso",
false, // create_parents (目录已存在)
true, // overwrite
)?;
```
#### 创建目录
```rust
pub fn create_directory(&mut self, path: &str, create_parents: bool) -> Result<()>
```
在数据分区中创建目录。
**示例:**
```rust
let mut img = VentoyImage::open(Path::new("ventoy.img"))?;
// 创建单级目录
img.create_directory("iso", false)?;
// 递归创建多级目录(类似 mkdir -p
img.create_directory("iso/linux/ubuntu", true)?;
```
#### 删除文件
```rust
pub fn remove_file(&mut self, name: &str) -> Result<()>
pub fn remove_path(&mut self, path: &str) -> Result<()>
pub fn remove_recursive(&mut self, path: &str) -> Result<()>
```
从数据分区删除文件或目录。
**示例:**
```rust
let mut img = VentoyImage::open(Path::new("ventoy.img"))?;
// 删除根目录的文件
img.remove_file("ubuntu.iso")?;
// 删除子目录中的文件或空目录
img.remove_path("iso/linux/ubuntu.iso")?;
img.remove_path("iso/empty-dir")?;
// 递归删除目录及其所有内容
img.remove_recursive("iso")?;
```
#### 获取分区布局
```rust
pub fn layout(&self) -> &PartitionLayout
```
获取分区布局信息。
**示例:**
```rust
let img = VentoyImage::open(Path::new("ventoy.img"))?;
let layout = img.layout();
println!("Data partition: {} MB", layout.data_size() / 1024 / 1024);
println!("EFI partition: {} MB", layout.efi_size_sectors * 512 / 1024 / 1024);
```
### ExfatFs
底层 exFAT 文件系统操作。
#### 打开文件系统
```rust
pub fn open(path: &Path, layout: &PartitionLayout) -> Result<Self>
```
**示例:**
```rust
use ventoy_img::exfat::ExfatFs;
let layout = PartitionLayout::calculate(8 * 1024 * 1024 * 1024)?;
let mut fs = ExfatFs::open(Path::new("ventoy.img"), &layout)?;
```
#### 写入文件
```rust
// 基本写入(根目录)
pub fn write_file(&mut self, name: &str, data: &[u8]) -> Result<()>
// 带覆盖选项
pub fn write_file_overwrite(&mut self, name: &str, data: &[u8], overwrite: bool) -> Result<()>
// 写入到指定路径
pub fn write_file_path(&mut self, path: &str, data: &[u8], create_parents: bool, overwrite: bool) -> Result<()>
```
**示例:**
```rust
// 写入到根目录
fs.write_file("config.txt", b"content")?;
// 覆盖已存在的文件
fs.write_file_overwrite("config.txt", b"new content", true)?;
// 写入到子目录(自动创建父目录)
fs.write_file_path("iso/linux/config.txt", b"content", true, false)?;
```
#### 读取文件
```rust
pub fn read_file(&mut self, name: &str) -> Result<Vec<u8>>
pub fn read_file_path(&mut self, path: &str) -> Result<Vec<u8>>
```
读取文件内容到内存。
**示例:**
```rust
// 从根目录读取
let data = fs.read_file("config.txt")?;
// 从子目录读取
let data = fs.read_file_path("iso/linux/config.txt")?;
println!("{}", String::from_utf8_lossy(&data));
```
#### 流式读取(大文件)
```rust
pub fn read_file_to_writer<W: Write>(&mut self, name: &str, writer: &mut W) -> Result<u64>
pub fn read_file_path_to_writer<W: Write>(&mut self, path: &str, writer: &mut W) -> Result<u64>
```
流式读取文件到 Writer适合大文件。返回读取的字节数。
**示例:**
```rust
use std::fs::File;
use std::io::BufWriter;
// 从镜像中提取文件
let mut output = BufWriter::new(File::create("extracted.iso")?);
let bytes = fs.read_file_to_writer("ubuntu.iso", &mut output)?;
println!("Extracted {} bytes", bytes);
// 从子目录流式读取
let mut output = Vec::new();
fs.read_file_path_to_writer("iso/linux/ubuntu.iso", &mut output)?;
```
#### 删除文件
```rust
pub fn delete_file(&mut self, name: &str) -> Result<()>
pub fn delete_path(&mut self, path: &str) -> Result<()>
pub fn delete_recursive(&mut self, path: &str) -> Result<()>
```
删除文件或目录并释放空间。
**示例:**
```rust
// 删除根目录的文件
fs.delete_file("config.txt")?;
// 删除子目录中的文件或空目录
fs.delete_path("iso/linux/config.txt")?;
// 递归删除目录
fs.delete_recursive("iso")?;
```
#### 创建目录
```rust
pub fn create_directory(&mut self, path: &str, create_parents: bool) -> Result<()>
```
**示例:**
```rust
// 创建单级目录
fs.create_directory("iso", false)?;
// 递归创建多级目录
fs.create_directory("iso/linux/ubuntu", true)?;
```
#### 列出文件
```rust
pub fn list_files(&mut self) -> Result<Vec<FileInfo>>
pub fn list_files_at(&mut self, path: &str) -> Result<Vec<FileInfo>>
pub fn list_files_recursive(&mut self) -> Result<Vec<FileInfo>>
```
**示例:**
```rust
// 列出根目录
let files = fs.list_files()?;
// 列出指定目录
let files = fs.list_files_at("iso/linux")?;
// 递归列出所有文件
let all_files = fs.list_files_recursive()?;
```
#### 流式写入(大文件)
```rust
pub fn write_file_from_reader<R: Read>(&mut self, name: &str, reader: &mut R, size: u64) -> Result<()>
pub fn write_file_from_reader_overwrite<R: Read>(&mut self, name: &str, reader: &mut R, size: u64, overwrite: bool) -> Result<()>
pub fn write_file_from_reader_path<R: Read>(&mut self, path: &str, reader: &mut R, size: u64, create_parents: bool, overwrite: bool) -> Result<()>
```
从 Reader 流式写入文件,适合大文件。
**示例:**
```rust
use std::fs::File;
use std::io::BufReader;
let file = File::open("large.iso")?;
let size = file.metadata()?.len();
let mut reader = BufReader::new(file);
// 写入到根目录
fs.write_file_from_reader("large.iso", &mut reader, size)?;
// 写入到子目录并覆盖
fs.write_file_from_reader_path(
"iso/linux/large.iso",
&mut reader,
size,
true, // create_parents
true, // overwrite
)?;
```
### ExfatFileWriter
手动控制的流式写入器。
```rust
use ventoy_img::exfat::ExfatFileWriter;
// 创建写入器(根目录)
let mut writer = ExfatFileWriter::create(&mut fs, "file.iso", total_size)?;
// 创建写入器(带覆盖选项)
let mut writer = ExfatFileWriter::create_overwrite(&mut fs, "file.iso", total_size, true)?;
// 创建写入器(指定路径,支持创建父目录和覆盖)
let mut writer = ExfatFileWriter::create_at_path(
&mut fs,
"iso/linux/file.iso",
total_size,
true, // create_parents
false, // overwrite
)?;
// 分块写入
loop {
let n = source.read(&mut buffer)?;
if n == 0 { break; }
writer.write(&buffer[..n])?;
}
// 完成写入(创建目录条目)
writer.finish()?;
```
### ExfatFileReader
手动控制的流式读取器,实现 `std::io::Read``std::io::Seek` 特征。
```rust
use ventoy_img::exfat::ExfatFileReader;
use std::io::{Read, Seek, SeekFrom};
// 打开文件读取器(根目录)
let mut reader = ExfatFileReader::open(&mut fs, "file.iso")?;
// 打开文件读取器(指定路径)
let mut reader = ExfatFileReader::open_path(&mut fs, "iso/linux/file.iso")?;
// 获取文件信息
println!("File size: {} bytes", reader.file_size());
println!("Current position: {}", reader.position());
println!("Remaining: {} bytes", reader.remaining());
// 读取数据
let mut buffer = [0u8; 4096];
let n = reader.read(&mut buffer)?;
// 读取全部内容
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
// Seek 操作
reader.seek(SeekFrom::Start(1000))?; // 从开头偏移
reader.seek(SeekFrom::Current(100))?; // 从当前位置偏移
reader.seek(SeekFrom::End(-100))?; // 从结尾偏移
// 精确读取
let mut exact_buffer = [0u8; 100];
reader.read_exact(&mut exact_buffer)?;
```
**特性:**
- 实现 `std::io::Read``std::io::Seek`
- 自动 cluster 缓存,减少 I/O 次数
- 支持任意位置的 seek
- 内存占用低,只缓存当前 cluster
## 错误处理
所有操作返回 `Result<T, VentoyError>`
```rust
use ventoy_img::{Result, VentoyError};
fn example() -> Result<()> {
let img = VentoyImage::open(Path::new("ventoy.img"))
.map_err(|e| {
match &e {
VentoyError::Io(io_err) => eprintln!("IO error: {}", io_err),
VentoyError::ImageError(msg) => eprintln!("Image error: {}", msg),
VentoyError::FilesystemError(msg) => eprintln!("FS error: {}", msg),
_ => eprintln!("Error: {}", e),
}
e
})?;
Ok(())
}
```
## 完整示例
### 创建镜像并添加 ISO
```rust
use ventoy_img::{VentoyImage, Result};
use std::path::Path;
fn main() -> Result<()> {
// 创建 16GB 镜像
println!("Creating Ventoy image...");
let mut img = VentoyImage::create(
Path::new("ventoy.img"),
"16G",
"Ventoy"
)?;
// 添加 ISO 文件
println!("Adding ISO files...");
img.add_file(Path::new("/isos/ubuntu-22.04.iso"))?;
img.add_file(Path::new("/isos/windows11.iso"))?;
// 列出文件
println!("\nFiles in image:");
for file in img.list_files()? {
println!(" {} ({:.1} MB)", file.name, file.size as f64 / 1024.0 / 1024.0);
}
println!("\nDone! Write ventoy.img to USB drive.");
Ok(())
}
```
### 批量处理 ISO 文件
```rust
use ventoy_img::{VentoyImage, Result};
use std::path::Path;
use std::fs;
fn main() -> Result<()> {
let iso_dir = Path::new("/path/to/isos");
// 计算所需大小
let total_size: u64 = fs::read_dir(iso_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "iso"))
.map(|e| e.metadata().map(|m| m.len()).unwrap_or(0))
.sum();
// 添加 1GB 余量 + 32MB EFI
let image_size = total_size + 1024 * 1024 * 1024 + 32 * 1024 * 1024;
let size_str = format!("{}M", image_size / 1024 / 1024);
// 创建镜像
let mut img = VentoyImage::create(Path::new("ventoy.img"), &size_str, "Ventoy")?;
// 添加所有 ISO
for entry in fs::read_dir(iso_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "iso") {
println!("Adding: ", path.display());
img.add_file(&path)?;
}
}
Ok(())
}
```
## 注意事项
1. **文件大小限制**: 单个文件最大支持 exFAT 限制(约 16 EB
2. **文件名**: 最长 255 个 UTF-16 字符,大小写不敏感
3. **Unicode 支持**: 完整支持国际字符中日韩、西里尔、希腊字母、Emoji 等)
4. **并发**: `ExfatFs` 不是线程安全的,需要外部同步
5. **磁盘空间**: 创建镜像时会预分配全部空间(稀疏文件)
6. **最小大小**: 镜像最小 64MB32MB EFI + 32MB 数据)
7. **动态簇大小**: 根据卷大小自动选择(<256MB: 4KB, 256MB-8GB: 32KB, >8GB: 128KB

View File

@@ -0,0 +1,510 @@
# ventoy-img 技术文档
## 概述
ventoy-img 是一个纯 Rust 实现的 Ventoy 可启动镜像生成工具,无需 root 权限或 loop 设备即可创建完整可用的 Ventoy IMG 文件。
## 架构设计
```
┌─────────────────────────────────────────────────────────────┐
│ CLI (main.rs) │
├─────────────────────────────────────────────────────────────┤
│ VentoyImage (image.rs) │
├──────────────┬──────────────┬──────────────┬───────────────┤
│ Partition │ exFAT │ Resources │ Error │
│ (partition.rs)│ (exfat/) │(resources.rs)│ (error.rs) │
└──────────────┴──────────────┴──────────────┴───────────────┘
```
## 核心模块
### 1. 分区模块 (partition.rs)
负责 MBR 分区表的创建和管理。
#### 分区布局
```
┌────────────────────────────────────────────────────────────┐
│ Sector 0: MBR (Boot Code + Partition Table + Signature) │
├────────────────────────────────────────────────────────────┤
│ Sector 1-2047: GRUB core.img (引导代码) │
├────────────────────────────────────────────────────────────┤
│ Sector 2048 - N: 数据分区 (exFAT, 存放 ISO 文件) │
├────────────────────────────────────────────────────────────┤
│ Sector N+1 - End: EFI 分区 (FAT16, 32MB, UEFI 启动) │
└────────────────────────────────────────────────────────────┘
```
#### MBR 结构 (512 字节)
| 偏移 | 大小 | 内容 |
|------|------|------|
| 0x000 | 440 | Boot Code (来自 boot.img) |
| 0x1B8 | 4 | Disk Signature |
| 0x1BC | 2 | Reserved |
| 0x1BE | 16 | Partition Entry 1 (数据分区) |
| 0x1CE | 16 | Partition Entry 2 (EFI 分区) |
| 0x1DE | 16 | Partition Entry 3 (未使用) |
| 0x1EE | 16 | Partition Entry 4 (未使用) |
| 0x1FE | 2 | Boot Signature (0x55AA) |
#### Ventoy 签名
位于 MBR 偏移 0x19016 字节:
```
56 54 00 47 65 00 48 44 00 52 64 00 20 45 72 0D
```
### 2. exFAT 模块 (exfat/)
完整的 exFAT 文件系统实现,支持读写操作。
#### 2.1 格式化 (format.rs)
创建 exFAT 文件系统结构:
```
┌─────────────────────────────────────────────────────────────┐
│ Boot Region (Sector 0-11) │
│ - Boot Sector (512 bytes) │
│ - Extended Boot Sectors (8 sectors) │
│ - OEM Parameters (2 sectors) │
│ - Boot Checksum Sector │
├─────────────────────────────────────────────────────────────┤
│ Backup Boot Region (Sector 12-23) │
├─────────────────────────────────────────────────────────────┤
│ FAT Region (Sector 24+) │
│ - FAT Table (每个 cluster 4 字节) │
├─────────────────────────────────────────────────────────────┤
│ Cluster Heap │
│ - Cluster 2: Allocation Bitmap │
│ - Cluster 3..N: Upcase Table (128KB可能跨多个簇) │
│ - Cluster N+1: Root Directory │
│ - Cluster N+2+: 用户数据 │
└─────────────────────────────────────────────────────────────┘
```
##### 动态簇大小
根据卷大小自动选择最优簇大小:
| 卷大小 | 簇大小 | 说明 |
|--------|--------|------|
| < 256MB | 4KB | 适合小文件减少浪费 |
| 256MB - 8GB | 32KB | 平衡性能和空间 |
| > 8GB | 128KB | 优化大文件 (ISO) 性能 |
```rust
fn get_cluster_size(total_sectors: u64) -> u32 {
let volume_size = total_sectors * 512;
match volume_size {
n if n < 256 * 1024 * 1024 => 4096, // < 256MB
n if n < 8 * 1024 * 1024 * 1024 => 32768, // 256MB - 8GB
_ => 128 * 1024, // > 8GB
}
}
```
#### Boot Sector 关键字段
| 偏移 | 大小 | 字段 | 说明 |
|------|------|------|------|
| 0 | 3 | JumpBoot | 跳转指令 (0xEB 0x76 0x90) |
| 3 | 8 | FileSystemName | "EXFAT " |
| 64 | 8 | PartitionOffset | 分区偏移 |
| 72 | 8 | VolumeLength | 卷大小(扇区数) |
| 80 | 4 | FatOffset | FAT 起始扇区 |
| 84 | 4 | FatLength | FAT 长度(扇区数) |
| 88 | 4 | ClusterHeapOffset | Cluster Heap 起始扇区 |
| 92 | 4 | ClusterCount | Cluster 总数 |
| 96 | 4 | FirstClusterOfRootDirectory | 根目录起始 Cluster |
| 100 | 4 | VolumeSerialNumber | 卷序列号 |
| 108 | 1 | BytesPerSectorShift | 扇区大小位移 (9 = 512) |
| 109 | 1 | SectorsPerClusterShift | Cluster 大小位移 |
| 510 | 2 | BootSignature | 0xAA55 |
#### 2.2 文件操作 (ops.rs)
##### Cluster 编号规则
- Cluster 0, 1: 保留
- Cluster 2: Allocation Bitmap
- Cluster 3: Upcase Table
- Cluster 4: Root Directory
- Cluster 5+: 用户数据
##### FAT 表条目值
| 值 | 含义 |
|----|------|
| 0x00000000 | 空闲 |
| 0x00000002 - 0xFFFFFFF6 | 下一个 Cluster |
| 0xFFFFFFF7 | 坏 Cluster |
| 0xFFFFFFF8 - 0xFFFFFFFF | 链结束 |
##### 目录条目类型
| 类型 | 值 | 说明 |
|------|-----|------|
| Volume Label | 0x83 | 卷标 |
| Allocation Bitmap | 0x81 | 位图描述 |
| Upcase Table | 0x82 | 大写表描述 |
| File | 0x85 | 文件/目录 |
| Stream Extension | 0xC0 | 流扩展 |
| File Name | 0xC1 | 文件名 |
##### 文件条目集结构
创建一个文件需要 3+ 个目录条目:
```
┌─────────────────────────────────────────────────────────────┐
│ File Directory Entry (0x85) - 32 bytes │
│ - EntryType: 0x85 │
│ - SecondaryCount: 后续条目数 │
│ - SetChecksum: 校验和 │
│ - FileAttributes: 属性 │
│ - Timestamps: 创建/修改/访问时间 │
├─────────────────────────────────────────────────────────────┤
│ Stream Extension Entry (0xC0) - 32 bytes │
│ - EntryType: 0xC0 │
│ - GeneralSecondaryFlags: 标志 │
│ - NameLength: 文件名长度 (UTF-16 字符数) │
│ - NameHash: 文件名哈希 │
│ - FirstCluster: 数据起始 Cluster │
│ - DataLength: 文件大小 │
├─────────────────────────────────────────────────────────────┤
│ File Name Entry (0xC1) - 32 bytes × N │
│ - EntryType: 0xC1 │
│ - FileName: 15 个 UTF-16 字符 │
└─────────────────────────────────────────────────────────────┘
```
##### 校验和算法
```rust
// Entry Set Checksum
fn calculate_entry_set_checksum(entries: &[[u8; 32]]) -> u16 {
let mut checksum: u16 = 0;
for (entry_idx, entry) in entries.iter().enumerate() {
for (byte_idx, &byte) in entry.iter().enumerate() {
// 跳过第一个条目的校验和字段 (bytes 2-3)
if entry_idx == 0 && (byte_idx == 2 || byte_idx == 3) {
continue;
}
checksum = checksum.rotate_right(1).wrapping_add(byte as u16);
}
}
checksum
}
// Name Hash (使用 Unicode 大写转换)
fn calculate_name_hash(name: &str) -> u16 {
let mut hash: u16 = 0;
for ch in name.encode_utf16() {
let upper = unicode::to_uppercase_simple(ch); // 使用 Unicode 模块
let bytes = upper.to_le_bytes();
hash = hash.rotate_right(1).wrapping_add(bytes[0] as u16);
hash = hash.rotate_right(1).wrapping_add(bytes[1] as u16);
}
hash
}
```
#### 2.3 Unicode 模块 (unicode.rs)
提供 exFAT 文件名的 Unicode 支持:
##### UTF-16 大写转换
支持以下字符范围的大小写转换:
- ASCII (a-z)
- Latin-1 Supplement (à-ÿ)
- Latin Extended-A (ā-ž)
- Greek (α-ω)
- Cyrillic (а-я, ѐ-џ)
```rust
pub fn to_uppercase_simple(ch: u16) -> u16 {
match ch {
0x0061..=0x007A => ch - 32, // ASCII a-z
0x00E0..=0x00F6 | 0x00F8..=0x00FE => ch - 32, // Latin-1
0x03B1..=0x03C1 => ch - 32, // Greek α-ρ
0x03C3..=0x03C9 => ch - 32, // Greek σ
0x03C2 => 0x03A3, // ς -> Σ
0x0430..=0x044F => ch - 32, // Cyrillic а
0x0450..=0x045F => ch - 80, // Cyrillic ѐ-џ
// ... 更多 Latin Extended-A 映射
_ => ch,
}
}
```
##### Upcase Table
生成 128KB 的 Upcase 表,映射每个 UTF-16 代码单元到其大写形式:
```rust
pub fn generate_upcase_table() -> Vec<u8> {
let mut table = Vec::with_capacity(65536 * 2);
for i in 0u32..65536 {
let upper = to_uppercase_simple(i as u16);
table.extend_from_slice(&upper.to_le_bytes());
}
table // 128KB
}
```
##### UTF-16 编解码
支持 BMP 和补充平面字符(如 Emoji
```rust
// 编码
pub fn encode_utf16le(s: &str) -> Vec<u8>
// 解码(处理代理对)
pub fn decode_utf16le(bytes: &[u8]) -> String
```
### 3. 资源模块 (resources.rs)
内嵌 Ventoy 启动所需的二进制资源:
| 资源 | 大小 | 用途 |
|------|------|------|
| boot.img | 512 bytes | MBR 引导代码 |
| core.img.xz | ~448 KB | GRUB 核心镜像 (XZ 压缩) |
| ventoy.disk.img.xz | ~13 MB | EFI 分区镜像 (XZ 压缩) |
资源使用 `include_bytes!` 宏在编译时嵌入,运行时使用 `lzma-rs` 解压。
### 4. 错误处理 (error.rs)
使用 `thiserror` 定义错误类型:
```rust
#[derive(Debug, thiserror::Error)]
pub enum VentoyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid size format: {0}")]
InvalidSize(String),
#[error("Image error: {0}")]
ImageError(String),
#[error("Filesystem error: {0}")]
FilesystemError(String),
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Decompression error: {0}")]
DecompressionError(String),
}
```
## 关键实现细节
### Cluster 大小选择
根据卷大小动态选择 cluster 大小:
- < 256MB: 4KB clusters (适合小文件)
- 256MB - 8GB: 32KB clusters (平衡)
- > 8GB: 128KB clusters (优化大文件性能)
Upcase Table 固定为 128KB (65536 × 2 bytes),可能跨多个 cluster。
### 流式读写
#### ExfatFileWriter
支持流式写入大文件:
```rust
pub struct ExfatFileWriter<'a> {
fs: &'a mut ExfatFs,
name: String,
total_size: u64, // 必须预先知道
allocated_clusters: Vec<u32>,
current_cluster_index: usize,
cluster_buffer: Vec<u8>, // 缓冲区(簇大小)
bytes_written: u64,
}
```
写入流程:
1. 预先分配所有需要的 clusters
2. 数据写入 cluster_buffer
3. 缓冲区满时写入当前 cluster
4. finish() 时创建目录条目
#### ExfatFileReader
支持流式读取大文件,实现 `std::io::Read``std::io::Seek`
```rust
pub struct ExfatFileReader<'a> {
fs: &'a mut ExfatFs,
cluster_chain: Vec<u32>, // 文件的 cluster 链
file_size: u64, // 文件总大小
position: u64, // 当前读取位置
cluster_cache: Option<(u32, Vec<u8>)>, // 当前 cluster 缓存
}
```
读取流程:
1. 根据 position 计算当前 cluster 索引和偏移
2. 如果 cluster 不在缓存中,读取并缓存
3. 从缓存中复制数据到用户缓冲区
4. 更新 position
Seek 支持:
- `SeekFrom::Start(n)` - 从文件开头偏移
- `SeekFrom::Current(n)` - 从当前位置偏移
- `SeekFrom::End(n)` - 从文件结尾偏移
### 文件名大小写
exFAT 文件名大小写不敏感但保留大小写:
- 查找时转换为小写比较
- 存储时保留原始大小写
- Name Hash 使用大写计算
## 性能考虑
1. **稀疏文件**: 使用 `file.set_len()` 创建稀疏文件,避免写入全零
2. **批量写入**: cluster 级别批量写入,减少 I/O 次数
3. **内存映射**: 未使用 mmap保持跨平台兼容性
4. **缓冲**: 流式写入使用 128KB 缓冲区
## 限制
1. ~~仅支持根目录文件操作~~ ✅ 已支持子目录
2. ~~不支持文件覆盖~~ ✅ 已支持文件覆盖
3. ~~目录条目限制在单个 cluster 内~~ ✅ 已支持目录扩展(多 cluster
4. ~~仅支持 ASCII 文件名~~ ✅ 已支持完整 Unicode中日韩、西里尔、希腊、Emoji
5. 不支持扩展属性和 ACL
## 新增功能
### 子目录支持
支持完整的目录操作:
- 路径解析:支持 `path/to/file` 格式
- 创建目录支持递归创建父目录mkdir -p
- 目录遍历:支持遍历多 cluster 的目录
- 递归列出:支持列出所有子目录中的文件
- 递归删除:支持删除目录及其所有内容
```rust
// 路径解析
fn parse_path(path: &str) -> Vec<&str> {
path.trim_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.collect()
}
// 解析路径到目标目录
fn resolve_path(&mut self, path: &str, create_parents: bool) -> Result<ResolvedPath>
```
### 文件覆盖支持
所有写入方法都支持覆盖选项:
- `write_file_overwrite()` - 覆盖根目录文件
- `write_file_path()` - 支持 `overwrite` 参数
- `ExfatFileWriter::create_overwrite()` - 流式写入覆盖
- `ExfatFileWriter::create_at_path()` - 指定路径 + 覆盖
覆盖逻辑:
1. 检查目标文件是否存在
2. 如果存在且 `overwrite=true`,先删除旧文件
3. 创建新文件
### 目录扩展支持
当目录中的文件数量超过单个 cluster 容量时,自动扩展目录:
```rust
fn find_free_slot_in_directory(&mut self, dir_cluster: u32, entries_needed: usize) -> Result<(u32, u32)> {
// 1. 遍历目录链中的所有 cluster
// 2. 查找连续的空闲条目
// 3. 如果空间不足,调用 extend_cluster_chain() 分配新 cluster
// 4. 清除旧 cluster 中的 END 标记
// 5. 返回新 cluster 和偏移
}
fn extend_cluster_chain(&mut self, first_cluster: u32) -> Result<u32> {
// 1. 读取 cluster 链,找到最后一个 cluster
// 2. 分配一个新 cluster
// 3. 更新 FAT 表链接
// 4. 初始化新 cluster 为零
// 5. 返回新 cluster 编号
}
```
### 流式读取支持
`ExfatFileReader` 支持流式读取大文件:
```rust
use ventoy_img::exfat::ExfatFileReader;
use std::io::{Read, Seek, SeekFrom};
// 打开文件
let mut reader = ExfatFileReader::open(&mut fs, "large.iso")?;
// 获取文件信息
println!("Size: {}, Position: {}", reader.file_size(), reader.position());
// 读取数据
let mut buf = vec![0u8; 4096];
let n = reader.read(&mut buf)?;
// Seek 操作
reader.seek(SeekFrom::Start(1024))?;
reader.seek(SeekFrom::Current(100))?;
reader.seek(SeekFrom::End(-100))?;
```
特性:
- 实现 `std::io::Read``std::io::Seek` 特征
- Cluster 级别缓存,减少 I/O
- 支持任意位置 seek
- 内存占用低(只缓存当前 cluster
### Unicode 支持
完整的 Unicode 文件名支持:
支持的字符范围:
- ASCII (a-z, A-Z)
- Latin-1 Supplement (à-ÿ, À-Þ)
- Latin Extended-A (ā-ž)
- Greek (α-ω, Α-Ω)
- Cyrillic (а-я, А-Я, ѐ-џ, Ѐ-Џ)
- CJK 字符(中日韩)
- Emoji通过 UTF-16 代理对)
```rust
// 支持 Unicode 文件名
fs.write_file("中文文件.txt", b"content")?;
fs.write_file("Файл.txt", b"content")?; // 俄语
fs.write_file("αβγ.txt", b"content")?; // 希腊语
fs.write_file("😀🎉.txt", b"content")?; // Emoji
// 大小写不敏感查找
let data = fs.read_file("ФАЙЛ.TXT")?; // 找到 Файл.txt
```
## 参考资料
- [exFAT File System Specification](https://docs.microsoft.com/en-us/windows/win32/fileio/exfat-specification)
- [Ventoy Official Documentation](https://www.ventoy.net/en/doc_start.html)
- [GRUB Manual](https://www.gnu.org/software/grub/manual/grub/)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,32 @@
//! Error types for ventoy-img
use thiserror::Error;
#[derive(Error, Debug)]
pub enum VentoyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid image size: {0}. Minimum is 64MB")]
InvalidSize(String),
#[error("Failed to parse size string: {0}")]
SizeParseError(String),
#[error("Partition error: {0}")]
PartitionError(String),
#[error("Filesystem error: {0}")]
FilesystemError(String),
#[error("Image not found or invalid: {0}")]
ImageError(String),
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Resource not found: {0}")]
ResourceNotFound(String),
}
pub type Result<T> = std::result::Result<T, VentoyError>;

View File

@@ -0,0 +1,432 @@
//! exFAT filesystem formatting
use crate::error::Result;
use crate::exfat::unicode;
use std::io::{Seek, SeekFrom, Write};
/// exFAT cluster size based on volume size
///
/// exFAT specification recommendations:
/// - < 256MB: 4KB clusters
/// - 256MB - 32GB: 32KB clusters
/// - 32GB - 256GB: 128KB clusters
/// - > 256GB: 256KB clusters (but we cap at 128KB for simplicity)
///
/// Note: Smaller clusters reduce waste but increase FAT table size and metadata overhead.
/// Larger clusters improve performance but waste space on small files.
fn get_cluster_size(total_sectors: u64) -> u32 {
let volume_size = total_sectors * 512; // Convert to bytes
match volume_size {
// < 256MB: Use 4KB clusters (good for many small files)
n if n < 256 * 1024 * 1024 => 4096,
// 256MB - 8GB: Use 32KB clusters (balanced)
n if n < 8 * 1024 * 1024 * 1024 => 32768,
// 8GB - 256GB: Use 128KB clusters (optimal for large ISOs)
_ => 128 * 1024,
}
}
/// Calculate sectors per cluster shift
///
/// Returns the power of 2 for sectors per cluster (512-byte sectors).
/// For example: 32KB cluster = 64 sectors = 2^6, so shift = 6
fn sectors_per_cluster_shift(cluster_size: u32) -> u8 {
match cluster_size {
4096 => 3, // 8 sectors (4KB)
8192 => 4, // 16 sectors (8KB)
16384 => 5, // 32 sectors (16KB)
32768 => 6, // 64 sectors (32KB)
65536 => 7, // 128 sectors (64KB)
131072 => 8, // 256 sectors (128KB)
262144 => 9, // 512 sectors (256KB)
_ => {
// Fallback: calculate dynamically
let sectors = cluster_size / 512;
(sectors.trailing_zeros() as u8).max(3).min(9)
}
}
}
/// exFAT Boot Sector (512 bytes)
#[repr(C, packed)]
struct ExfatBootSector {
jump_boot: [u8; 3],
fs_name: [u8; 8],
must_be_zero: [u8; 53],
partition_offset: u64,
volume_length: u64,
fat_offset: u32,
fat_length: u32,
cluster_heap_offset: u32,
cluster_count: u32,
first_cluster_of_root: u32,
volume_serial_number: u32,
fs_revision: u16,
volume_flags: u16,
bytes_per_sector_shift: u8,
sectors_per_cluster_shift: u8,
number_of_fats: u8,
drive_select: u8,
percent_in_use: u8,
reserved: [u8; 7],
boot_code: [u8; 390],
boot_signature: u16,
}
impl ExfatBootSector {
fn new(
volume_length: u64,
cluster_size: u32,
volume_serial: u32,
) -> Self {
let sector_size: u32 = 512;
let sectors_per_cluster = cluster_size / sector_size;
let spc_shift = sectors_per_cluster_shift(cluster_size);
// Calculate FAT offset (after boot region, typically sector 24)
let fat_offset: u32 = 24;
// Calculate cluster count and FAT length
// Cluster heap starts after FAT region
let usable_sectors = volume_length as u32 - fat_offset;
let cluster_count = (usable_sectors - 32) / sectors_per_cluster; // rough estimate
let fat_entries = cluster_count + 2; // cluster 0 and 1 are reserved
let fat_length = ((fat_entries * 4 + sector_size - 1) / sector_size).max(1);
// Cluster heap offset
let cluster_heap_offset = fat_offset + fat_length;
// Recalculate cluster count
let heap_sectors = volume_length as u32 - cluster_heap_offset;
let cluster_count = heap_sectors / sectors_per_cluster;
// Calculate root directory cluster based on upcase table size
// Cluster 2: Bitmap (1 cluster)
// Cluster 3...: Upcase table (128KB, may span multiple clusters)
// Next available: Root directory
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
let upcase_clusters = ((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
let first_cluster_of_root = 3 + upcase_clusters;
Self {
jump_boot: [0xEB, 0x76, 0x90],
fs_name: *b"EXFAT ",
must_be_zero: [0; 53],
partition_offset: 0,
volume_length,
fat_offset,
fat_length,
cluster_heap_offset,
cluster_count,
first_cluster_of_root,
volume_serial_number: volume_serial,
fs_revision: 0x0100,
volume_flags: 0,
bytes_per_sector_shift: 9, // 512 bytes
sectors_per_cluster_shift: spc_shift,
number_of_fats: 1,
drive_select: 0x80,
percent_in_use: 0xFF,
reserved: [0; 7],
boot_code: [0; 390],
boot_signature: 0xAA55,
}
}
fn to_bytes(&self) -> [u8; 512] {
let mut bytes = [0u8; 512];
bytes[0..3].copy_from_slice(&self.jump_boot);
bytes[3..11].copy_from_slice(&self.fs_name);
// bytes[11..64] already zero (must_be_zero)
bytes[64..72].copy_from_slice(&self.partition_offset.to_le_bytes());
bytes[72..80].copy_from_slice(&self.volume_length.to_le_bytes());
bytes[80..84].copy_from_slice(&self.fat_offset.to_le_bytes());
bytes[84..88].copy_from_slice(&self.fat_length.to_le_bytes());
bytes[88..92].copy_from_slice(&self.cluster_heap_offset.to_le_bytes());
bytes[92..96].copy_from_slice(&self.cluster_count.to_le_bytes());
bytes[96..100].copy_from_slice(&self.first_cluster_of_root.to_le_bytes());
bytes[100..104].copy_from_slice(&self.volume_serial_number.to_le_bytes());
bytes[104..106].copy_from_slice(&self.fs_revision.to_le_bytes());
bytes[106..108].copy_from_slice(&self.volume_flags.to_le_bytes());
bytes[108] = self.bytes_per_sector_shift;
bytes[109] = self.sectors_per_cluster_shift;
bytes[110] = self.number_of_fats;
bytes[111] = self.drive_select;
bytes[112] = self.percent_in_use;
// bytes[113..120] reserved
// bytes[120..510] boot_code
bytes[510..512].copy_from_slice(&self.boot_signature.to_le_bytes());
bytes
}
}
/// Calculate boot checksum for exFAT
fn calculate_boot_checksum(sectors: &[[u8; 512]; 11]) -> u32 {
let mut checksum: u32 = 0;
for (sector_idx, sector) in sectors.iter().enumerate() {
for (byte_idx, &byte) in sector.iter().enumerate() {
// Skip VolumeFlags and PercentInUse fields in boot sector
if sector_idx == 0 && (byte_idx == 106 || byte_idx == 107 || byte_idx == 112) {
continue;
}
checksum = if checksum & 1 != 0 {
0x80000000 | (checksum >> 1)
} else {
checksum >> 1
};
checksum = checksum.wrapping_add(byte as u32);
}
}
checksum
}
/// Upcase table with Unicode support
///
/// Uses the unicode module for proper uppercase conversion
/// of international characters (Latin Extended, Greek, Cyrillic, etc.)
fn generate_upcase_table() -> Vec<u8> {
unicode::generate_upcase_table()
}
/// Calculate upcase table checksum
fn calculate_upcase_checksum(data: &[u8]) -> u32 {
let mut checksum: u32 = 0;
for &byte in data {
checksum = if checksum & 1 != 0 {
0x80000000 | (checksum >> 1)
} else {
checksum >> 1
};
checksum = checksum.wrapping_add(byte as u32);
}
checksum
}
/// Directory entry types
const ENTRY_TYPE_VOLUME_LABEL: u8 = 0x83;
const ENTRY_TYPE_BITMAP: u8 = 0x81;
const ENTRY_TYPE_UPCASE: u8 = 0x82;
/// Create volume label directory entry
fn create_volume_label_entry(label: &str) -> [u8; 32] {
let mut entry = [0u8; 32];
entry[0] = ENTRY_TYPE_VOLUME_LABEL;
let label_chars: Vec<u16> = label.encode_utf16().take(11).collect();
entry[1] = label_chars.len() as u8;
for (i, &ch) in label_chars.iter().enumerate() {
let offset = 2 + i * 2;
entry[offset..offset + 2].copy_from_slice(&ch.to_le_bytes());
}
entry
}
/// Create bitmap directory entry
fn create_bitmap_entry(start_cluster: u32, size: u64) -> [u8; 32] {
let mut entry = [0u8; 32];
entry[0] = ENTRY_TYPE_BITMAP;
entry[1] = 0; // BitmapFlags
// Reserved: bytes 2-19
entry[20..24].copy_from_slice(&start_cluster.to_le_bytes());
entry[24..32].copy_from_slice(&size.to_le_bytes());
entry
}
/// Create upcase table directory entry
fn create_upcase_entry(start_cluster: u32, size: u64, checksum: u32) -> [u8; 32] {
let mut entry = [0u8; 32];
entry[0] = ENTRY_TYPE_UPCASE;
// Reserved: bytes 1-3
entry[4..8].copy_from_slice(&checksum.to_le_bytes());
// Reserved: bytes 8-19
entry[20..24].copy_from_slice(&start_cluster.to_le_bytes());
entry[24..32].copy_from_slice(&size.to_le_bytes());
entry
}
/// Format a partition as exFAT
pub fn format_exfat<W: Write + Seek>(
writer: &mut W,
partition_offset: u64,
partition_size: u64,
label: &str,
) -> Result<()> {
let volume_sectors = partition_size / 512;
let cluster_size = get_cluster_size(volume_sectors);
let _sectors_per_cluster = cluster_size / 512;
// Generate volume serial from timestamp
let serial = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as u32)
.unwrap_or(0x12345678);
// Create boot sector
let boot_sector = ExfatBootSector::new(volume_sectors, cluster_size, serial);
let boot_bytes = boot_sector.to_bytes();
// Prepare boot region (12 sectors)
let mut boot_region: [[u8; 512]; 11] = [[0; 512]; 11];
boot_region[0] = boot_bytes;
// Sectors 1-8: Extended boot sectors (can be zero)
// Sector 9-10: OEM parameters (can be zero)
// Calculate boot checksum
let checksum = calculate_boot_checksum(&boot_region);
let mut checksum_sector = [0u8; 512];
for i in 0..128 {
checksum_sector[i * 4..(i + 1) * 4].copy_from_slice(&checksum.to_le_bytes());
}
// Write main boot region (sectors 0-11)
writer.seek(SeekFrom::Start(partition_offset))?;
for sector in &boot_region {
writer.write_all(sector)?;
}
writer.write_all(&checksum_sector)?;
// Write backup boot region (sectors 12-23)
for sector in &boot_region {
writer.write_all(sector)?;
}
writer.write_all(&checksum_sector)?;
// Write FAT
let fat_offset = partition_offset + boot_sector.fat_offset as u64 * 512;
writer.seek(SeekFrom::Start(fat_offset))?;
// Calculate how many clusters the upcase table needs (128KB)
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
let upcase_clusters = ((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
let root_cluster = 3 + upcase_clusters; // Root comes after bitmap and upcase
// FAT entries: cluster 0 and 1 are reserved
// 0: Media type (0xFFFFFFF8)
// 1: Reserved (0xFFFFFFFF)
// 2: Bitmap cluster (single cluster, end of chain)
// 3..3+upcase_clusters-1: Upcase table cluster chain
// 3+upcase_clusters: Root directory cluster (end of chain)
let mut fat_entries = vec![
0xFFFFFFF8, // Media type
0xFFFFFFFF, // Reserved
0xFFFFFFFF, // Bitmap (single cluster, end of chain)
];
// Build upcase table cluster chain
for i in 0..upcase_clusters {
let cluster_num = 3 + i;
if i == upcase_clusters - 1 {
// Last cluster in chain
fat_entries.push(0xFFFFFFFF);
} else {
// Point to next cluster
fat_entries.push(cluster_num + 1);
}
}
// Root directory (single cluster, end of chain)
fat_entries.push(0xFFFFFFFF);
for entry in &fat_entries {
writer.write_all(&entry.to_le_bytes())?;
}
// Zero fill rest of FAT
let fat_remaining = (boot_sector.fat_length as usize * 512) - (fat_entries.len() * 4);
writer.write_all(&vec![0u8; fat_remaining])?;
// Calculate cluster heap offset
let heap_offset = partition_offset + boot_sector.cluster_heap_offset as u64 * 512;
// Cluster 2: Allocation Bitmap
let bitmap_size = (boot_sector.cluster_count + 7) / 8;
let _bitmap_clusters = ((bitmap_size as u64 + cluster_size as u64 - 1) / cluster_size as u64).max(1);
let mut bitmap = vec![0u8; cluster_size as usize];
// Mark clusters 2, 3..3+upcase_clusters-1, root_cluster as used
// Cluster 2: bitmap
bitmap[0] |= 0b00000100; // Bit 2
// Clusters 3..3+upcase_clusters-1: upcase table
for i in 0..upcase_clusters {
let cluster = 3 + i;
let byte_idx = (cluster / 8) as usize;
let bit_idx = cluster % 8;
if byte_idx < bitmap.len() {
bitmap[byte_idx] |= 1 << bit_idx;
}
}
// Root directory cluster
let byte_idx = (root_cluster / 8) as usize;
let bit_idx = root_cluster % 8;
if byte_idx < bitmap.len() {
bitmap[byte_idx] |= 1 << bit_idx;
}
writer.seek(SeekFrom::Start(heap_offset))?;
writer.write_all(&bitmap)?;
// Cluster 3..3+upcase_clusters-1: Upcase table
let upcase_data = generate_upcase_table();
let upcase_checksum = calculate_upcase_checksum(&upcase_data);
let upcase_offset = heap_offset + cluster_size as u64; // Start at cluster 3
writer.seek(SeekFrom::Start(upcase_offset))?;
writer.write_all(&upcase_data)?;
// Pad to fill all upcase clusters
let upcase_total_size = upcase_clusters as usize * cluster_size as usize;
let upcase_padding = upcase_total_size - upcase_data.len();
if upcase_padding > 0 {
writer.write_all(&vec![0u8; upcase_padding])?;
}
// Root directory cluster
let root_offset = heap_offset + (1 + upcase_clusters as u64) * cluster_size as u64;
writer.seek(SeekFrom::Start(root_offset))?;
// Write directory entries
let volume_label_entry = create_volume_label_entry(label);
let bitmap_entry = create_bitmap_entry(2, bitmap_size as u64);
let upcase_entry = create_upcase_entry(3, upcase_data.len() as u64, upcase_checksum);
writer.write_all(&volume_label_entry)?;
writer.write_all(&bitmap_entry)?;
writer.write_all(&upcase_entry)?;
// Pad root directory to cluster size
let root_used = 32 * 3;
let root_padding = cluster_size as usize - root_used;
writer.write_all(&vec![0u8; root_padding])?;
writer.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cluster_size() {
// < 256MB: 4KB clusters
assert_eq!(get_cluster_size(200 * 2048), 4096); // 100MB → 4KB
assert_eq!(get_cluster_size(100 * 1024 * 1024 / 512), 4096); // 100MB → 4KB
// 256MB - 8GB: 32KB clusters
assert_eq!(get_cluster_size(512 * 1024 * 1024 / 512), 32768); // 512MB → 32KB
assert_eq!(get_cluster_size(4 * 1024 * 1024 * 1024 / 512), 32768); // 4GB → 32KB
// >= 8GB: 128KB clusters
assert_eq!(get_cluster_size(8 * 1024 * 1024 * 1024 / 512), 131072); // 8GB → 128KB
assert_eq!(get_cluster_size(16 * 1024 * 1024 * 1024 / 512), 131072); // 16GB → 128KB
}
}

View File

@@ -0,0 +1,8 @@
//! exFAT filesystem module
pub mod format;
pub mod ops;
pub mod unicode;
pub use format::format_exfat;
pub use ops::{ExfatFileReader, ExfatFileWriter, ExfatFs, FileInfo};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,284 @@
//! Unicode support for exFAT filesystem
//!
//! exFAT uses UTF-16LE encoding for file names and requires Unicode-aware
//! case-insensitive comparison. This module provides:
//! - Unicode uppercase conversion for name hash calculation
//! - Upcase table generation
//! - Unicode-aware file name comparison
/// Convert a UTF-16 code unit to uppercase
///
/// This function handles:
/// - ASCII letters (a-z)
/// - Latin Extended characters (à-ÿ, etc.)
/// - Greek letters (α-ω)
/// - Cyrillic letters (а-я)
/// - And other commonly used Unicode letters
///
/// For full Unicode support, we use Rust's built-in char::to_uppercase(),
/// but for exFAT name hash we need a simpler mapping that matches the upcase table.
pub fn to_uppercase_simple(ch: u16) -> u16 {
match ch {
// ASCII lowercase (a-z)
0x0061..=0x007A => ch - 32,
// Latin-1 Supplement lowercase letters (à-ö, ø-ÿ)
0x00E0..=0x00F6 | 0x00F8..=0x00FE => ch - 32,
// Latin Extended-A (selected common mappings)
0x0101 => 0x0100, // ā -> Ā
0x0103 => 0x0102, // ă -> Ă
0x0105 => 0x0104, // ą -> Ą
0x0107 => 0x0106, // ć -> Ć
0x0109 => 0x0108, // ĉ -> Ĉ
0x010B => 0x010A, // ċ -> Ċ
0x010D => 0x010C, // č -> Č
0x010F => 0x010E, // ď -> Ď
0x0111 => 0x0110, // đ -> Đ
0x0113 => 0x0112, // ē -> Ē
0x0115 => 0x0114, // ĕ -> Ĕ
0x0117 => 0x0116, // ė -> Ė
0x0119 => 0x0118, // ę -> Ę
0x011B => 0x011A, // ě -> Ě
0x011D => 0x011C, // ĝ -> Ĝ
0x011F => 0x011E, // ğ -> Ğ
0x0121 => 0x0120, // ġ -> Ġ
0x0123 => 0x0122, // ģ -> Ģ
0x0125 => 0x0124, // ĥ -> Ĥ
0x0127 => 0x0126, // ħ -> Ħ
0x0129 => 0x0128, // ĩ -> Ĩ
0x012B => 0x012A, // ī -> Ī
0x012D => 0x012C, // ĭ -> Ĭ
0x012F => 0x012E, // į -> Į
0x0131 => 0x0049, // ı -> I (Turkish dotless i)
0x0133 => 0x0132, // ij -> IJ
0x0135 => 0x0134, // ĵ -> Ĵ
0x0137 => 0x0136, // ķ -> Ķ
0x013A => 0x0139, // ĺ -> Ĺ
0x013C => 0x013B, // ļ -> Ļ
0x013E => 0x013D, // ľ -> Ľ
0x0140 => 0x013F, // ŀ -> Ŀ
0x0142 => 0x0141, // ł -> Ł
0x0144 => 0x0143, // ń -> Ń
0x0146 => 0x0145, // ņ -> Ņ
0x0148 => 0x0147, // ň -> Ň
0x014B => 0x014A, // ŋ -> Ŋ
0x014D => 0x014C, // ō -> Ō
0x014F => 0x014E, // ŏ -> Ŏ
0x0151 => 0x0150, // ő -> Ő
0x0153 => 0x0152, // œ -> Œ
0x0155 => 0x0154, // ŕ -> Ŕ
0x0157 => 0x0156, // ŗ -> Ŗ
0x0159 => 0x0158, // ř -> Ř
0x015B => 0x015A, // ś -> Ś
0x015D => 0x015C, // ŝ -> Ŝ
0x015F => 0x015E, // ş -> Ş
0x0161 => 0x0160, // š -> Š
0x0163 => 0x0162, // ţ -> Ţ
0x0165 => 0x0164, // ť -> Ť
0x0167 => 0x0166, // ŧ -> Ŧ
0x0169 => 0x0168, // ũ -> Ũ
0x016B => 0x016A, // ū -> Ū
0x016D => 0x016C, // ŭ -> Ŭ
0x016F => 0x016E, // ů -> Ů
0x0171 => 0x0170, // ű -> Ű
0x0173 => 0x0172, // ų -> Ų
0x0175 => 0x0174, // ŵ -> Ŵ
0x0177 => 0x0176, // ŷ -> Ŷ
0x017A => 0x0179, // ź -> Ź
0x017C => 0x017B, // ż -> Ż
0x017E => 0x017D, // ž -> Ž
0x017F => 0x0053, // ſ -> S (long s)
// Greek lowercase (α-ω and variants)
0x03B1..=0x03C1 => ch - 32, // α-ρ -> Α-Ρ
0x03C3..=0x03C9 => ch - 32, // σ-ω -> Σ-Ω
0x03C2 => 0x03A3, // ς (final sigma) -> Σ
// Cyrillic lowercase (а-я)
0x0430..=0x044F => ch - 32, // а-я -> А
// Cyrillic Extended (ѐ-џ)
0x0450..=0x045F => ch - 80, // ѐ-џ -> Ѐ-Џ
// No conversion needed
_ => ch,
}
}
/// Generate the exFAT upcase table
///
/// The upcase table maps every UTF-16 code unit (0x0000-0xFFFF) to its
/// uppercase equivalent. This is used by the filesystem for case-insensitive
/// file name comparison.
///
/// Returns a 128KB table (65536 entries × 2 bytes each)
pub fn generate_upcase_table() -> Vec<u8> {
let mut table = Vec::with_capacity(65536 * 2);
for i in 0u32..65536 {
let upper = to_uppercase_simple(i as u16);
table.extend_from_slice(&upper.to_le_bytes());
}
table
}
/// Calculate exFAT name hash
///
/// The name hash is a 16-bit value stored in the Stream Extension entry,
/// used for fast file name lookup. It's calculated from the uppercase
/// version of each UTF-16 character.
pub fn calculate_name_hash(name: &str) -> u16 {
let mut hash: u16 = 0;
for ch in name.encode_utf16() {
let upper = to_uppercase_simple(ch);
let bytes = upper.to_le_bytes();
hash = hash.rotate_right(1).wrapping_add(bytes[0] as u16);
hash = hash.rotate_right(1).wrapping_add(bytes[1] as u16);
}
hash
}
/// Compare two file names in a case-insensitive manner
///
/// This uses Unicode-aware lowercase comparison (via Rust's str::to_lowercase)
/// which is appropriate for user-facing file name matching.
pub fn names_equal_ignore_case(name1: &str, name2: &str) -> bool {
name1.to_lowercase() == name2.to_lowercase()
}
/// Encode a string as UTF-16LE bytes
pub fn encode_utf16le(s: &str) -> Vec<u8> {
let mut bytes = Vec::new();
for ch in s.encode_utf16() {
bytes.extend_from_slice(&ch.to_le_bytes());
}
bytes
}
/// Decode UTF-16LE bytes to a String
///
/// Handles surrogate pairs for characters outside the BMP (like emoji)
pub fn decode_utf16le(bytes: &[u8]) -> String {
if bytes.len() % 2 != 0 {
return String::new();
}
let code_units: Vec<u16> = bytes
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.take_while(|&c| c != 0) // Stop at null terminator
.collect();
String::from_utf16_lossy(&code_units)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ascii_uppercase() {
assert_eq!(to_uppercase_simple(b'a' as u16), b'A' as u16);
assert_eq!(to_uppercase_simple(b'z' as u16), b'Z' as u16);
assert_eq!(to_uppercase_simple(b'A' as u16), b'A' as u16);
assert_eq!(to_uppercase_simple(b'0' as u16), b'0' as u16);
}
#[test]
fn test_latin_extended_uppercase() {
// é -> É
assert_eq!(to_uppercase_simple(0x00E9), 0x00C9);
// ñ -> Ñ
assert_eq!(to_uppercase_simple(0x00F1), 0x00D1);
// ü -> Ü
assert_eq!(to_uppercase_simple(0x00FC), 0x00DC);
}
#[test]
fn test_greek_uppercase() {
// α -> Α
assert_eq!(to_uppercase_simple(0x03B1), 0x0391);
// ω -> Ω
assert_eq!(to_uppercase_simple(0x03C9), 0x03A9);
// ς (final sigma) -> Σ
assert_eq!(to_uppercase_simple(0x03C2), 0x03A3);
}
#[test]
fn test_cyrillic_uppercase() {
// а -> А
assert_eq!(to_uppercase_simple(0x0430), 0x0410);
// я -> Я
assert_eq!(to_uppercase_simple(0x044F), 0x042F);
}
#[test]
fn test_name_hash() {
// Same hash for different cases
let hash1 = calculate_name_hash("Test.txt");
let hash2 = calculate_name_hash("TEST.TXT");
let hash3 = calculate_name_hash("test.txt");
assert_eq!(hash1, hash2);
assert_eq!(hash2, hash3);
}
#[test]
fn test_name_hash_unicode() {
// Unicode names should produce consistent hashes
let hash1 = calculate_name_hash("Привет.txt"); // Russian
let hash2 = calculate_name_hash("ПРИВЕТ.TXT");
assert_eq!(hash1, hash2);
}
#[test]
fn test_utf16_encoding() {
// ASCII
let encoded = encode_utf16le("Test");
assert_eq!(encoded, vec![b'T', 0, b'e', 0, b's', 0, b't', 0]);
// CJK character (中)
let encoded = encode_utf16le("");
assert_eq!(encoded, vec![0x2D, 0x4E]); // U+4E2D in little-endian
// Emoji (😀) - surrogate pair
let encoded = encode_utf16le("😀");
// U+1F600 = D83D DE00 (surrogate pair)
assert_eq!(encoded, vec![0x3D, 0xD8, 0x00, 0xDE]);
}
#[test]
fn test_utf16_decoding() {
// ASCII
let decoded = decode_utf16le(&[b'T', 0, b'e', 0, b's', 0, b't', 0]);
assert_eq!(decoded, "Test");
// CJK character
let decoded = decode_utf16le(&[0x2D, 0x4E]);
assert_eq!(decoded, "");
// With null terminator
let decoded = decode_utf16le(&[b'H', 0, b'i', 0, 0, 0, b'X', 0]);
assert_eq!(decoded, "Hi");
// Emoji (surrogate pair)
let decoded = decode_utf16le(&[0x3D, 0xD8, 0x00, 0xDE]);
assert_eq!(decoded, "😀");
}
#[test]
fn test_names_equal_ignore_case() {
assert!(names_equal_ignore_case("Test.txt", "TEST.TXT"));
assert!(names_equal_ignore_case("файл.txt", "ФАЙЛ.TXT")); // Russian
assert!(!names_equal_ignore_case("Test1.txt", "Test2.txt"));
}
#[test]
fn test_upcase_table_size() {
let table = generate_upcase_table();
assert_eq!(table.len(), 65536 * 2); // 128KB
}
}

View File

@@ -0,0 +1,268 @@
//! Ventoy image creation and management
use crate::error::{Result, VentoyError};
use crate::exfat::{format_exfat, ExfatFs, FileInfo};
use crate::partition::{
parse_size, write_mbr_partition_table, PartitionLayout, SECTOR_SIZE, VENTOY_SIG_OFFSET,
};
use crate::resources::{get_boot_img, get_core_img, get_ventoy_disk_img, VENTOY_SIGNATURE};
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
/// Ventoy image builder and manager
pub struct VentoyImage {
path: std::path::PathBuf,
layout: PartitionLayout,
}
impl VentoyImage {
/// Create a new Ventoy IMG file
pub fn create(path: &Path, size_str: &str, label: &str) -> Result<Self> {
let size = parse_size(size_str)?;
let layout = PartitionLayout::calculate(size)?;
println!("[INFO] Creating {}MB image: {}", size / (1024 * 1024), path.display());
// Create sparse file
let mut file = File::create(path)?;
file.set_len(size)?;
// Write boot code
println!("[INFO] Writing boot code...");
Self::write_boot_code(&mut file)?;
// Write partition table
println!("[INFO] Writing MBR partition table...");
println!(
" Data partition: sector {} - {} ({} MB)",
layout.data_start_sector,
layout.data_start_sector + layout.data_size_sectors - 1,
layout.data_size() / (1024 * 1024)
);
println!(
" EFI partition: sector {} - {} (32 MB)",
layout.efi_start_sector,
layout.efi_start_sector + layout.efi_size_sectors - 1
);
write_mbr_partition_table(&mut file, &layout)?;
// Write Ventoy signature
println!("[INFO] Writing Ventoy signature...");
Self::write_ventoy_signature(&mut file)?;
// Write EFI partition
println!("[INFO] Writing EFI partition...");
Self::write_efi_partition(&mut file, &layout)?;
// Format data partition as exFAT
println!("[INFO] Formatting data partition as exFAT...");
format_exfat(&mut file, layout.data_offset(), layout.data_size(), label)?;
file.flush()?;
println!("[INFO] Ventoy IMG created successfully!");
Ok(Self {
path: path.to_path_buf(),
layout,
})
}
/// Open an existing Ventoy IMG file
pub fn open(path: &Path) -> Result<Self> {
let mut file = OpenOptions::new().read(true).write(true).open(path)?;
// Verify Ventoy signature
let mut sig = [0u8; 16];
file.seek(SeekFrom::Start(VENTOY_SIG_OFFSET))?;
file.read_exact(&mut sig)?;
if sig != VENTOY_SIGNATURE {
return Err(VentoyError::ImageError(format!(
"Invalid Ventoy signature in {}",
path.display()
)));
}
// Get file size and calculate layout
let size = file.metadata()?.len();
let layout = PartitionLayout::calculate(size)?;
Ok(Self {
path: path.to_path_buf(),
layout,
})
}
/// Write boot code (boot.img + core.img)
fn write_boot_code(file: &mut File) -> Result<()> {
// Write boot.img MBR code (first 440 bytes)
let boot_img = get_boot_img()?;
file.seek(SeekFrom::Start(0))?;
file.write_all(&boot_img[..440])?;
// Write core.img (sector 1-2047)
let core_img = get_core_img()?;
file.seek(SeekFrom::Start(SECTOR_SIZE))?;
let max_size = 2047 * SECTOR_SIZE as usize;
let write_size = core_img.len().min(max_size);
file.write_all(&core_img[..write_size])?;
Ok(())
}
/// Write Ventoy signature
fn write_ventoy_signature(file: &mut File) -> Result<()> {
file.seek(SeekFrom::Start(VENTOY_SIG_OFFSET))?;
file.write_all(&VENTOY_SIGNATURE)?;
Ok(())
}
/// Write EFI partition content
fn write_efi_partition(file: &mut File, layout: &PartitionLayout) -> Result<()> {
let efi_img = get_ventoy_disk_img()?;
file.seek(SeekFrom::Start(layout.efi_offset()))?;
let max_size = (layout.efi_size_sectors * SECTOR_SIZE) as usize;
let write_size = efi_img.len().min(max_size);
file.write_all(&efi_img[..write_size])?;
Ok(())
}
/// Get partition layout
pub fn layout(&self) -> &PartitionLayout {
&self.layout
}
/// List files in the data partition (root directory)
pub fn list_files(&self) -> Result<Vec<FileInfo>> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.list_files()
}
/// List files in a specific directory
pub fn list_files_at(&self, path: &str) -> Result<Vec<FileInfo>> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.list_files_at(path)
}
/// List all files recursively
pub fn list_files_recursive(&self) -> Result<Vec<FileInfo>> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.list_files_recursive()
}
/// Add a file to the data partition (root directory)
pub fn add_file(&mut self, src_path: &Path) -> Result<()> {
let name = src_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| VentoyError::FilesystemError("Invalid filename".to_string()))?;
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
// Use streaming write for efficiency
let mut src_file = File::open(src_path)?;
let size = src_file.metadata()?.len();
fs.write_file_from_reader(name, &mut src_file, size)
}
/// Add a file to the data partition with overwrite option
pub fn add_file_overwrite(&mut self, src_path: &Path, overwrite: bool) -> Result<()> {
let name = src_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| VentoyError::FilesystemError("Invalid filename".to_string()))?;
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
let mut src_file = File::open(src_path)?;
let size = src_file.metadata()?.len();
fs.write_file_from_reader_overwrite(name, &mut src_file, size, overwrite)
}
/// Add a file to a specific path in the data partition
///
/// # Arguments
/// * `src_path` - Source file path on the local filesystem
/// * `dest_path` - Destination path in the image (e.g., "iso/linux/ubuntu.iso")
/// * `create_parents` - If true, creates intermediate directories as needed
/// * `overwrite` - If true, overwrites existing files
pub fn add_file_to_path(
&mut self,
src_path: &Path,
dest_path: &str,
create_parents: bool,
overwrite: bool,
) -> Result<()> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
let mut src_file = File::open(src_path)?;
let size = src_file.metadata()?.len();
fs.write_file_from_reader_path(dest_path, &mut src_file, size, create_parents, overwrite)
}
/// Create a directory in the data partition
///
/// # Arguments
/// * `path` - Directory path to create (e.g., "iso/linux")
/// * `create_parents` - If true, creates intermediate directories (mkdir -p behavior)
pub fn create_directory(&mut self, path: &str, create_parents: bool) -> Result<()> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.create_directory(path, create_parents)
}
/// Remove a file from the data partition (root directory)
pub fn remove_file(&mut self, name: &str) -> Result<()> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.delete_file(name)
}
/// Remove a file or empty directory at a specific path
pub fn remove_path(&mut self, path: &str) -> Result<()> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.delete_path(path)
}
/// Remove a file or directory recursively
pub fn remove_recursive(&mut self, path: &str) -> Result<()> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.delete_recursive(path)
}
/// Read a file from the data partition
pub fn read_file(&self, path: &str) -> Result<Vec<u8>> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.read_file_path(path)
}
/// Read a file from the data partition to a writer (streaming)
///
/// This is the preferred method for large files as it doesn't load
/// the entire file into memory.
pub fn read_file_to_writer<W: std::io::Write>(&self, path: &str, writer: &mut W) -> Result<u64> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.read_file_path_to_writer(path, writer)
}
/// Get file information without reading the content
///
/// Returns file size, name, and whether it's a directory.
/// Returns None if the file doesn't exist.
pub fn get_file_info(&self, path: &str) -> Result<Option<FileInfo>> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.get_file_info_path(path)
}
/// Get image path
pub fn path(&self) -> &Path {
&self.path
}
}

View File

@@ -0,0 +1,48 @@
//! Ventoy IMG Generator
//!
//! A Rust library for creating and managing Ventoy bootable IMG files
//! without requiring root privileges or loop devices.
//!
//! # Features
//!
//! - Create Ventoy IMG files with MBR partition table
//! - Format data partition as exFAT
//! - Add, list, read, and remove files in the data partition
//! - Load boot resources from external files
//!
//! # Example
//!
//! ```no_run
//! use ventoy_img::{VentoyImage, resources};
//! use std::path::Path;
//!
//! // Initialize resources from data directory
//! resources::init_resources(Path::new("/var/lib/one-kvm/ventoy")).unwrap();
//!
//! // Create a new 8GB Ventoy image
//! let mut image = VentoyImage::create(
//! Path::new("ventoy.img"),
//! "8G",
//! "Ventoy"
//! ).unwrap();
//!
//! // Add an ISO file
//! image.add_file(Path::new("/path/to/ubuntu.iso")).unwrap();
//!
//! // List files
//! for file in image.list_files().unwrap() {
//! println!("{}: {} bytes", file.name, file.size);
//! }
//! ```
pub mod error;
pub mod exfat;
pub mod image;
pub mod partition;
pub mod resources;
pub use error::{Result, VentoyError};
pub use exfat::FileInfo;
pub use image::VentoyImage;
pub use partition::{parse_size, PartitionLayout};
pub use resources::{init_resources, get_resource_dir, is_initialized, required_files};

View File

@@ -0,0 +1,263 @@
//! Ventoy IMG CLI
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::process::ExitCode;
use ventoy_img::{VentoyImage, Result, VentoyError};
#[derive(Parser)]
#[command(name = "ventoy-img")]
#[command(version, about = "Create and manage Ventoy bootable IMG files")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Create a new Ventoy IMG file
Create {
/// Image size (e.g., 8G, 16G, 1024M)
#[arg(short, long, default_value = "8G")]
size: String,
/// Output file path
#[arg(short, long, default_value = "ventoy.img")]
output: PathBuf,
/// Volume label for data partition
#[arg(short = 'L', long, default_value = "Ventoy")]
label: String,
},
/// Add a file (ISO/IMG) to Ventoy image
Add {
/// Ventoy IMG file
image: PathBuf,
/// File to add
file: PathBuf,
/// Destination path in image (e.g., "iso/linux/ubuntu.iso")
#[arg(short, long)]
dest: Option<String>,
/// Overwrite existing file
#[arg(short, long)]
force: bool,
/// Create parent directories as needed
#[arg(short, long)]
parents: bool,
},
/// List files in Ventoy image
List {
/// Ventoy IMG file
image: PathBuf,
/// Directory path to list (default: root)
#[arg(long)]
path: Option<String>,
/// List files recursively
#[arg(short, long)]
recursive: bool,
},
/// Remove a file or directory from Ventoy image
Remove {
/// Ventoy IMG file
image: PathBuf,
/// Path to remove (file or directory)
path: String,
/// Remove directories and their contents recursively
#[arg(short, long)]
recursive: bool,
},
/// Create a directory in Ventoy image
Mkdir {
/// Ventoy IMG file
image: PathBuf,
/// Directory path to create
path: String,
/// Create parent directories as needed
#[arg(short, long)]
parents: bool,
},
/// Show image information
Info {
/// Ventoy IMG file
image: PathBuf,
},
}
fn main() -> ExitCode {
let cli = Cli::parse();
let result = match cli.command {
Commands::Create { size, output, label } => cmd_create(&output, &size, &label),
Commands::Add { image, file, dest, force, parents } => cmd_add(&image, &file, dest.as_deref(), force, parents),
Commands::List { image, path, recursive } => cmd_list(&image, path.as_deref(), recursive),
Commands::Remove { image, path, recursive } => cmd_remove(&image, &path, recursive),
Commands::Mkdir { image, path, parents } => cmd_mkdir(&image, &path, parents),
Commands::Info { image } => cmd_info(&image),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("[ERROR] {}", e);
ExitCode::FAILURE
}
}
}
fn cmd_create(output: &PathBuf, size: &str, label: &str) -> Result<()> {
println!("========================================");
println!(" Ventoy IMG Creator (Rust Edition)");
println!("========================================");
println!();
VentoyImage::create(output, size, label)?;
println!();
println!("========================================");
println!("Image: {}", output.display());
println!("Size: {}", size);
println!("Label: {}", label);
println!("========================================");
Ok(())
}
fn cmd_add(image: &PathBuf, file: &PathBuf, dest: Option<&str>, force: bool, parents: bool) -> Result<()> {
if !file.exists() {
return Err(VentoyError::FileNotFound(file.display().to_string()));
}
let mut img = VentoyImage::open(image)?;
match dest {
Some(dest_path) => {
// Add to specific path
img.add_file_to_path(file, dest_path, parents, force)?;
println!("Added {} -> {}", file.display(), dest_path);
}
None => {
// Add to root
if force {
img.add_file_overwrite(file, true)?;
} else {
img.add_file(file)?;
}
println!("Added {}", file.display());
}
}
Ok(())
}
fn cmd_list(image: &PathBuf, path: Option<&str>, recursive: bool) -> Result<()> {
let img = VentoyImage::open(image)?;
let files = if recursive {
img.list_files_recursive()?
} else {
match path {
Some(p) => img.list_files_at(p)?,
None => img.list_files()?,
}
};
if files.is_empty() {
println!("No files in image");
return Ok(());
}
if recursive {
println!("{:<50} {:>15} {}", "PATH", "SIZE", "TYPE");
println!("{}", "-".repeat(70));
for file in files {
let type_str = if file.is_directory { "DIR" } else { "FILE" };
let size_str = format_size(file.size);
println!("{:<50} {:>15} {}", file.path, size_str, type_str);
}
} else {
println!("{:<40} {:>15} {}", "NAME", "SIZE", "TYPE");
println!("{}", "-".repeat(60));
for file in files {
let type_str = if file.is_directory { "DIR" } else { "FILE" };
let size_str = format_size(file.size);
println!("{:<40} {:>15} {}", file.name, size_str, type_str);
}
}
Ok(())
}
fn cmd_remove(image: &PathBuf, path: &str, recursive: bool) -> Result<()> {
let mut img = VentoyImage::open(image)?;
if recursive {
img.remove_recursive(path)?;
println!("Removed {} (recursive)", path);
} else {
img.remove_path(path)?;
println!("Removed {}", path);
}
Ok(())
}
fn cmd_mkdir(image: &PathBuf, path: &str, parents: bool) -> Result<()> {
let mut img = VentoyImage::open(image)?;
img.create_directory(path, parents)?;
println!("Created directory: {}", path);
Ok(())
}
fn cmd_info(image: &PathBuf) -> Result<()> {
let img = VentoyImage::open(image)?;
let layout = img.layout();
println!("Image: {}", image.display());
println!();
println!("Partition Layout:");
println!(" Data partition:");
println!(" Start: sector {} (offset {})",
layout.data_start_sector,
format_size(layout.data_offset()));
println!(" Size: {} sectors ({})",
layout.data_size_sectors,
format_size(layout.data_size()));
println!(" EFI partition:");
println!(" Start: sector {} (offset {})",
layout.efi_start_sector,
format_size(layout.efi_offset()));
println!(" Size: {} sectors (32 MB)",
layout.efi_size_sectors);
Ok(())
}
fn format_size(bytes: u64) -> String {
if bytes >= 1024 * 1024 * 1024 {
format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
} else if bytes >= 1024 * 1024 {
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
} else if bytes >= 1024 {
format!("{:.1} KB", bytes as f64 / 1024.0)
} else {
format!("{} B", bytes)
}
}

View File

@@ -0,0 +1,191 @@
//! MBR partition table implementation
use crate::error::{Result, VentoyError};
use std::io::{Seek, SeekFrom, Write};
/// Sector size in bytes
pub const SECTOR_SIZE: u64 = 512;
/// Data partition starts at sector 2048 (1MB aligned)
pub const DATA_PART_START_SECTOR: u64 = 2048;
/// EFI partition size: 32MB = 65536 sectors
pub const EFI_PART_SIZE_SECTORS: u64 = 65536;
/// Minimum image size: 64MB
pub const MIN_IMAGE_SIZE: u64 = 64 * 1024 * 1024;
/// MBR partition type: NTFS/exFAT (0x07)
pub const MBR_TYPE_EXFAT: u8 = 0x07;
/// MBR partition type: EFI System (0xEF)
pub const MBR_TYPE_EFI: u8 = 0xEF;
/// Ventoy signature offset in MBR
pub const VENTOY_SIG_OFFSET: u64 = 0x190; // 400
/// Partition layout information
#[derive(Debug, Clone)]
pub struct PartitionLayout {
pub total_sectors: u64,
pub data_start_sector: u64,
pub data_size_sectors: u64,
pub efi_start_sector: u64,
pub efi_size_sectors: u64,
}
impl PartitionLayout {
/// Calculate partition layout for given image size
pub fn calculate(total_size: u64) -> Result<Self> {
if total_size < MIN_IMAGE_SIZE {
return Err(VentoyError::InvalidSize(format!(
"{}MB (minimum 64MB)",
total_size / (1024 * 1024)
)));
}
let total_sectors = total_size / SECTOR_SIZE;
// EFI partition at the end, 4KB aligned
let efi_start = ((total_sectors - EFI_PART_SIZE_SECTORS) / 8) * 8;
// Data partition fills the gap
let data_size = efi_start - DATA_PART_START_SECTOR;
Ok(Self {
total_sectors,
data_start_sector: DATA_PART_START_SECTOR,
data_size_sectors: data_size,
efi_start_sector: efi_start,
efi_size_sectors: EFI_PART_SIZE_SECTORS,
})
}
/// Get data partition offset in bytes
pub fn data_offset(&self) -> u64 {
self.data_start_sector * SECTOR_SIZE
}
/// Get data partition size in bytes
pub fn data_size(&self) -> u64 {
self.data_size_sectors * SECTOR_SIZE
}
/// Get EFI partition offset in bytes
pub fn efi_offset(&self) -> u64 {
self.efi_start_sector * SECTOR_SIZE
}
}
/// MBR partition entry (16 bytes)
#[repr(C, packed)]
#[derive(Clone, Copy, Default)]
struct MbrPartitionEntry {
boot_indicator: u8,
start_chs: [u8; 3],
partition_type: u8,
end_chs: [u8; 3],
start_lba: u32,
size_sectors: u32,
}
impl MbrPartitionEntry {
fn new(bootable: bool, partition_type: u8, start_lba: u64, size_sectors: u64) -> Self {
Self {
boot_indicator: if bootable { 0x80 } else { 0x00 },
start_chs: [0xFE, 0xFF, 0xFF], // LBA mode
partition_type,
end_chs: [0xFE, 0xFF, 0xFF], // LBA mode
start_lba: start_lba as u32,
size_sectors: size_sectors as u32,
}
}
fn to_bytes(&self) -> [u8; 16] {
let mut bytes = [0u8; 16];
bytes[0] = self.boot_indicator;
bytes[1..4].copy_from_slice(&self.start_chs);
bytes[4] = self.partition_type;
bytes[5..8].copy_from_slice(&self.end_chs);
bytes[8..12].copy_from_slice(&self.start_lba.to_le_bytes());
bytes[12..16].copy_from_slice(&self.size_sectors.to_le_bytes());
bytes
}
}
/// Write MBR partition table to image
pub fn write_mbr_partition_table<W: Write + Seek>(
writer: &mut W,
layout: &PartitionLayout,
) -> Result<()> {
// Partition 1: Data partition (exFAT, bootable)
let part1 = MbrPartitionEntry::new(
true,
MBR_TYPE_EXFAT,
layout.data_start_sector,
layout.data_size_sectors,
);
// Partition 2: EFI System partition
let part2 = MbrPartitionEntry::new(
false,
MBR_TYPE_EFI,
layout.efi_start_sector,
layout.efi_size_sectors,
);
// Write partition table entries (offset 0x1BE = 446)
writer.seek(SeekFrom::Start(446))?;
writer.write_all(&part1.to_bytes())?;
writer.write_all(&part2.to_bytes())?;
// Clear partition 3 and 4
writer.write_all(&[0u8; 32])?;
// Write MBR signature (0x55AA)
writer.seek(SeekFrom::Start(510))?;
writer.write_all(&[0x55, 0xAA])?;
Ok(())
}
/// Parse size string like "8G", "1024M" into bytes
pub fn parse_size(s: &str) -> Result<u64> {
let s = s.trim().to_uppercase();
let (num_str, multiplier) = if s.ends_with('G') {
(&s[..s.len() - 1], 1024 * 1024 * 1024u64)
} else if s.ends_with('M') {
(&s[..s.len() - 1], 1024 * 1024u64)
} else if s.ends_with('K') {
(&s[..s.len() - 1], 1024u64)
} else {
(s.as_str(), 1u64)
};
let num: u64 = num_str
.parse()
.map_err(|_| VentoyError::SizeParseError(s.clone()))?;
Ok(num * multiplier)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_size() {
assert_eq!(parse_size("8G").unwrap(), 8 * 1024 * 1024 * 1024);
assert_eq!(parse_size("1024M").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_size("512K").unwrap(), 512 * 1024);
}
#[test]
fn test_partition_layout() {
let layout = PartitionLayout::calculate(8 * 1024 * 1024 * 1024).unwrap();
assert_eq!(layout.data_start_sector, 2048);
assert_eq!(layout.efi_size_sectors, 65536);
assert!(layout.efi_start_sector > layout.data_start_sector);
}
}

View File

@@ -0,0 +1,201 @@
//! Ventoy resources loader
//!
//! Loads Ventoy boot resources from external files in a resource directory.
//! Resource files (boot.img, core.img, ventoy.disk.img) should be pre-decompressed.
use crate::error::{Result, VentoyError};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
/// Resource file names
const BOOT_IMG_NAME: &str = "boot.img";
const CORE_IMG_NAME: &str = "core.img";
const VENTOY_DISK_IMG_NAME: &str = "ventoy.disk.img";
/// Ventoy signature (16 bytes at MBR offset 0x190)
pub const VENTOY_SIGNATURE: [u8; 16] = [
0x56, 0x54, 0x00, 0x47, 0x65, 0x00, 0x48, 0x44, 0x00, 0x52, 0x64, 0x00, 0x20, 0x45, 0x72, 0x0D,
];
/// Cached resources loaded from disk
struct ResourceCache {
boot_img: Vec<u8>,
core_img: Vec<u8>,
ventoy_disk_img: Vec<u8>,
}
/// Global resource cache
static RESOURCE_CACHE: OnceLock<ResourceCache> = OnceLock::new();
/// Initialize resources from a directory
///
/// This function must be called before using `get_boot_img()`, `get_core_img()`,
/// or `get_ventoy_disk_img()`. It loads all resource files into memory.
///
/// # Arguments
/// * `resource_dir` - Path to directory containing boot.img, core.img, ventoy.disk.img
///
/// # Example
/// ```no_run
/// use ventoy_img::resources::init_resources;
/// use std::path::Path;
///
/// init_resources(Path::new("/var/lib/one-kvm/ventoy")).unwrap();
/// ```
pub fn init_resources(resource_dir: &Path) -> Result<()> {
if RESOURCE_CACHE.get().is_some() {
// Already initialized
return Ok(());
}
let boot_path = resource_dir.join(BOOT_IMG_NAME);
let core_path = resource_dir.join(CORE_IMG_NAME);
let ventoy_disk_path = resource_dir.join(VENTOY_DISK_IMG_NAME);
// Check all files exist
if !boot_path.exists() {
return Err(VentoyError::ResourceNotFound(format!(
"boot.img not found at {}",
boot_path.display()
)));
}
if !core_path.exists() {
return Err(VentoyError::ResourceNotFound(format!(
"core.img not found at {}",
core_path.display()
)));
}
if !ventoy_disk_path.exists() {
return Err(VentoyError::ResourceNotFound(format!(
"ventoy.disk.img not found at {}",
ventoy_disk_path.display()
)));
}
// Load files
let boot_img = fs::read(&boot_path).map_err(|e| {
VentoyError::ResourceNotFound(format!("Failed to read {}: {}", boot_path.display(), e))
})?;
let core_img = fs::read(&core_path).map_err(|e| {
VentoyError::ResourceNotFound(format!("Failed to read {}: {}", core_path.display(), e))
})?;
let ventoy_disk_img = fs::read(&ventoy_disk_path).map_err(|e| {
VentoyError::ResourceNotFound(format!(
"Failed to read {}: {}",
ventoy_disk_path.display(),
e
))
})?;
// Validate boot.img size
if boot_img.len() != 512 {
return Err(VentoyError::ResourceNotFound(format!(
"boot.img has invalid size: {} bytes (expected 512)",
boot_img.len()
)));
}
let cache = ResourceCache {
boot_img,
core_img,
ventoy_disk_img,
};
// Try to set the cache (ignore if already set by another thread)
let _ = RESOURCE_CACHE.set(cache);
Ok(())
}
/// Check if resources have been initialized
pub fn is_initialized() -> bool {
RESOURCE_CACHE.get().is_some()
}
/// Get the boot.img data (512 bytes MBR boot code)
pub fn get_boot_img() -> Result<&'static [u8]> {
RESOURCE_CACHE
.get()
.map(|c| c.boot_img.as_slice())
.ok_or_else(|| {
VentoyError::ResourceNotFound(
"Resources not initialized. Call init_resources() first.".to_string(),
)
})
}
/// Get the core.img data (GRUB core image, ~1MB)
pub fn get_core_img() -> Result<&'static [u8]> {
RESOURCE_CACHE
.get()
.map(|c| c.core_img.as_slice())
.ok_or_else(|| {
VentoyError::ResourceNotFound(
"Resources not initialized. Call init_resources() first.".to_string(),
)
})
}
/// Get the ventoy.disk.img data (EFI partition, ~32MB)
pub fn get_ventoy_disk_img() -> Result<&'static [u8]> {
RESOURCE_CACHE
.get()
.map(|c| c.ventoy_disk_img.as_slice())
.ok_or_else(|| {
VentoyError::ResourceNotFound(
"Resources not initialized. Call init_resources() first.".to_string(),
)
})
}
/// Get the resource directory path for a given data directory
///
/// Returns `{data_dir}/ventoy`
pub fn get_resource_dir(data_dir: &Path) -> PathBuf {
data_dir.join("ventoy")
}
/// List required resource files
pub fn required_files() -> &'static [&'static str] {
&[BOOT_IMG_NAME, CORE_IMG_NAME, VENTOY_DISK_IMG_NAME]
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn create_test_resources(dir: &Path) {
// Create boot.img (512 bytes)
let mut boot = std::fs::File::create(dir.join(BOOT_IMG_NAME)).unwrap();
boot.write_all(&[0u8; 512]).unwrap();
// Create core.img (fake, 1KB)
let mut core = std::fs::File::create(dir.join(CORE_IMG_NAME)).unwrap();
core.write_all(&[0u8; 1024]).unwrap();
// Create ventoy.disk.img (fake, 1KB)
let mut ventoy = std::fs::File::create(dir.join(VENTOY_DISK_IMG_NAME)).unwrap();
ventoy.write_all(&[0u8; 1024]).unwrap();
}
#[test]
fn test_required_files() {
let files = required_files();
assert_eq!(files.len(), 3);
assert!(files.contains(&"boot.img"));
assert!(files.contains(&"core.img"));
assert!(files.contains(&"ventoy.disk.img"));
}
#[test]
fn test_get_resource_dir() {
let data_dir = Path::new("/var/lib/one-kvm");
let resource_dir = get_resource_dir(data_dir);
assert_eq!(resource_dir, PathBuf::from("/var/lib/one-kvm/ventoy"));
}
}