mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-02-02 11:01:53 +08:00
init
This commit is contained in:
32
libs/ventoy-img-rs/src/error.rs
Normal file
32
libs/ventoy-img-rs/src/error.rs
Normal 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>;
|
||||
432
libs/ventoy-img-rs/src/exfat/format.rs
Normal file
432
libs/ventoy-img-rs/src/exfat/format.rs
Normal 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
|
||||
}
|
||||
}
|
||||
8
libs/ventoy-img-rs/src/exfat/mod.rs
Normal file
8
libs/ventoy-img-rs/src/exfat/mod.rs
Normal 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};
|
||||
2059
libs/ventoy-img-rs/src/exfat/ops.rs
Normal file
2059
libs/ventoy-img-rs/src/exfat/ops.rs
Normal file
File diff suppressed because it is too large
Load Diff
284
libs/ventoy-img-rs/src/exfat/unicode.rs
Normal file
284
libs/ventoy-img-rs/src/exfat/unicode.rs
Normal 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
|
||||
}
|
||||
}
|
||||
268
libs/ventoy-img-rs/src/image.rs
Normal file
268
libs/ventoy-img-rs/src/image.rs
Normal 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
|
||||
}
|
||||
}
|
||||
48
libs/ventoy-img-rs/src/lib.rs
Normal file
48
libs/ventoy-img-rs/src/lib.rs
Normal 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};
|
||||
263
libs/ventoy-img-rs/src/main.rs
Normal file
263
libs/ventoy-img-rs/src/main.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
191
libs/ventoy-img-rs/src/partition.rs
Normal file
191
libs/ventoy-img-rs/src/partition.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
201
libs/ventoy-img-rs/src/resources.rs
Normal file
201
libs/ventoy-img-rs/src/resources.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user