Browse Source

feat: 创建磁盘镜像并格式化 (#74)

* feat: 创建磁盘镜像并格式化

Signed-off-by: longjin <longjin@dragonos.org>

---------

Signed-off-by: longjin <longjin@dragonos.org>
LoGin 5 months ago
parent
commit
c6f35e8aa5

+ 0 - 1
Cargo.toml

@@ -9,4 +9,3 @@ members = [
 [profile.release]
 lto = true
 opt-level = 3
-strip = true

+ 8 - 1
dadk-config/src/rootfs/mod.rs

@@ -1,19 +1,25 @@
 pub mod fstype;
+pub mod partition;
+
 mod utils;
 
 use std::{fs, path::PathBuf};
 
 use anyhow::Result;
 use fstype::FsType;
+use partition::PartitionConfig;
 use serde::Deserialize;
 
 /// rootfs配置文件
-#[derive(Debug, Clone, Copy, Deserialize)]
+#[derive(Debug, Clone, Deserialize)]
 pub struct RootFSConfigFile {
     pub metadata: RootFSMeta,
+    #[serde(default)]
+    pub partition: PartitionConfig,
 }
 
 impl RootFSConfigFile {
+    pub const LBA_SIZE: usize = 512;
     pub fn load(path: &PathBuf) -> Result<Self> {
         // 读取文件内容
         let content = fs::read_to_string(path)?;
@@ -32,6 +38,7 @@ pub struct RootFSMeta {
     /// rootfs文件系统类型
     pub fs_type: FsType,
     /// rootfs磁盘大小(至少要大于这个值)
+    /// 单位:字节
     #[serde(deserialize_with = "utils::size::deserialize_size")]
     pub size: usize,
 }

+ 48 - 0
dadk-config/src/rootfs/partition.rs

@@ -0,0 +1,48 @@
+use serde::Deserialize;
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
+pub struct PartitionConfig {
+    #[serde(rename = "type")]
+    pub partition_type: PartitionType,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
+pub enum PartitionType {
+    /// Disk image is not partitioned
+    #[default]
+    #[serde(rename = "none")]
+    None,
+    /// Use MBR partition table
+    #[serde(rename = "mbr")]
+    Mbr,
+    /// Use GPT partition table
+    #[serde(rename = "gpt")]
+    Gpt,
+}
+
+impl PartitionConfig {
+    /// Determines whether a partitioned image should be created.
+    ///
+    /// Returns `true` if the partition type is not `None`, otherwise returns `false`.
+    pub fn should_create_partitioned_image(&self) -> bool {
+        self.partition_type != PartitionType::None
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    #[test]
+    fn test_parse_partition_type() {
+        let test_cases = vec![
+            (r#"type = "none""#, PartitionType::None),
+            (r#"type = "mbr""#, PartitionType::Mbr),
+            (r#"type = "gpt""#, PartitionType::Gpt),
+        ];
+
+        for (config_content, expected_type) in test_cases {
+            let partition_config: PartitionConfig = toml::from_str(config_content).unwrap();
+            assert_eq!(partition_config.partition_type, expected_type);
+        }
+    }
+}

+ 6 - 2
dadk-config/src/rootfs/utils/size.rs

@@ -1,5 +1,7 @@
 use serde::Deserializer;
 
+use crate::rootfs::RootFSConfigFile;
+
 /// 自定义反序列化函数,用于解析表示磁盘镜像大小的值。
 ///
 /// 此函数支持两种输入格式:
@@ -27,7 +29,7 @@ where
     let value = serde::de::Deserialize::deserialize(deserializer)?;
 
     // 匹配输入值的类型,进行相应的转换
-    match value {
+    let r = match value {
         toml::Value::Integer(num) => {
             // 如果是整数类型,直接转换成usize
             Ok(num as usize)
@@ -38,7 +40,9 @@ where
                 .ok_or_else(|| serde::de::Error::custom("Invalid string for size"))
         }
         _ => Err(serde::de::Error::custom("Invalid type for size")),
-    }
+    };
+
+    r.map(|size| (size + RootFSConfigFile::LBA_SIZE - 1) & !(RootFSConfigFile::LBA_SIZE - 1))
 }
 
 /// Parses a size string with optional unit suffix (K, M, G) into a usize value.

+ 9 - 0
dadk-config/templates/config/rootfs.toml

@@ -3,3 +3,12 @@
 fs_type = "fat32"
 # Size of the rootfs disk image (eg, `1G`, `1024M`)
 size = "1G"
+
+[partition]
+# Partition type (options: "none", "mbr", "gpt")
+#
+# If "none" is specified, no partition table will be created, 
+# and the entire disk will be treated as a single partition.
+#
+# Note that the "none" option is incompatible with GRUB boot.
+type = "none"

+ 6 - 2
dadk-config/tests/test_rootfs_config.rs

@@ -1,4 +1,7 @@
-use dadk_config::{self, rootfs::RootFSConfigFile};
+use dadk_config::{
+    self,
+    rootfs::{partition::PartitionType, RootFSConfigFile},
+};
 use test_base::{
     dadk_config::DadkConfigTestContext,
     test_context::{self as test_context, test_context},
@@ -13,7 +16,8 @@ fn test_load_rootfs_manifest_template(ctx: &DadkConfigTestContext) {
     let rootfs_manifest_path = ctx.templates_dir().join(ROOTFS_CONFIG_FILE_NAME);
     assert_eq!(rootfs_manifest_path.exists(), true);
     assert_eq!(rootfs_manifest_path.is_file(), true);
-    let _manifest =
+    let manifest =
         RootFSConfigFile::load(&rootfs_manifest_path).expect("Failed to load rootfs manifest");
+    assert_eq!(manifest.partition.partition_type, PartitionType::None);
     // TODO 校验 manifest 中的字段是否齐全
 }

+ 3 - 0
dadk/Cargo.toml

@@ -35,3 +35,6 @@ dadk-user = { path = "../dadk-user" }
 derive_builder = "0.20.0"
 env_logger = "0.11.5"
 log = "0.4.22"
+
+[dev-dependencies]
+tempfile = "3.13.0"

+ 3 - 2
dadk/src/actions/mod.rs

@@ -1,5 +1,6 @@
 use crate::context::DADKExecContext;
 
+pub mod rootfs;
 pub mod user;
 
 pub fn run(ctx: DADKExecContext) {
@@ -7,8 +8,8 @@ pub fn run(ctx: DADKExecContext) {
         crate::console::Action::Kernel => {
             unimplemented!("kernel command has not implemented for run yet.")
         }
-        crate::console::Action::Rootfs(_rootfs_command) => {
-            unimplemented!("rootfs command has not implemented for run yet.")
+        crate::console::Action::Rootfs(rootfs_command) => {
+            rootfs::run(&ctx, rootfs_command).expect("Run rootfs action error.")
         }
         crate::console::Action::User(user_command) => {
             user::run(&ctx, user_command).expect("Run user action error.")

+ 289 - 0
dadk/src/actions/rootfs/disk_img.rs

@@ -0,0 +1,289 @@
+use std::{fs::File, io::Write, path::PathBuf, process::Command};
+
+use crate::context::DADKExecContext;
+use anyhow::{anyhow, Result};
+use dadk_config::rootfs::{fstype::FsType, partition::PartitionType};
+
+use super::loopdev::LoopDeviceBuilder;
+pub(super) fn create(ctx: &DADKExecContext) -> Result<()> {
+    let disk_image_path = ctx.disk_image_path();
+    if disk_image_path.exists() {
+        return Err(anyhow!(
+            "Disk image already exists: {}",
+            disk_image_path.display()
+        ));
+    }
+
+    disk_path_safety_check(&disk_image_path)?;
+
+    // 获取镜像大小
+    let image_size = ctx.disk_image_size();
+    create_raw_img(&disk_image_path, image_size).expect("Failed to create raw disk image");
+
+    // 判断是否需要分区?
+
+    let r = if ctx.rootfs().partition.should_create_partitioned_image() {
+        create_partitioned_image(ctx, &disk_image_path)
+    } else {
+        create_unpartitioned_image(ctx, &disk_image_path)
+    };
+
+    if r.is_err() {
+        std::fs::remove_file(&disk_image_path).expect("Failed to remove disk image");
+    }
+    r
+}
+/// Ensures the provided disk image path is not a device node.
+fn disk_path_safety_check(disk_image_path: &PathBuf) -> Result<()> {
+    const DONT_ALLOWED_PREFIX: [&str; 5] =
+        ["/dev/sd", "/dev/hd", "/dev/vd", "/dev/nvme", "/dev/mmcblk"];
+    let path = disk_image_path.to_str().ok_or(anyhow!(
+        "disk path safety check failed: disk path is not valid utf-8"
+    ))?;
+
+    DONT_ALLOWED_PREFIX.iter().for_each(|prefix| {
+        if path.starts_with(prefix) {
+            panic!("disk path safety check failed: disk path is not allowed to be a device node(except loop dev)");
+        }
+    });
+    Ok(())
+}
+
+fn create_partitioned_image(ctx: &DADKExecContext, disk_image_path: &PathBuf) -> Result<()> {
+    let part_type = ctx.rootfs().partition.partition_type;
+    DiskPartitioner::create_partitioned_image(disk_image_path, part_type)?;
+    // 挂载loop设备
+    let mut loop_device = LoopDeviceBuilder::new()
+        .img_path(disk_image_path.clone())
+        .build()
+        .map_err(|e| anyhow!("Failed to create loop device: {}", e))?;
+
+    let partition_path = loop_device.partition_path(1)?;
+    let fs_type = ctx.rootfs().metadata.fs_type;
+    DiskFormatter::format_disk(&partition_path, &fs_type)?;
+    loop_device.detach()?;
+    Ok(())
+}
+
+fn create_unpartitioned_image(ctx: &DADKExecContext, disk_image_path: &PathBuf) -> Result<()> {
+    // 直接对整块磁盘镜像进行格式化
+    let fs_type = ctx.rootfs().metadata.fs_type;
+    DiskFormatter::format_disk(disk_image_path, &fs_type)
+}
+
+/// 创建全0的raw镜像
+fn create_raw_img(disk_image_path: &PathBuf, image_size: usize) -> Result<()> {
+    log::trace!("Creating raw disk image: {}", disk_image_path.display());
+    // 创建父目录
+    if let Some(parent) = disk_image_path.parent() {
+        log::trace!("Creating parent directory: {}", parent.display());
+        std::fs::create_dir_all(parent)?;
+    }
+    // 打开或创建文件
+    let mut file = File::create(disk_image_path)?;
+
+    // 将文件大小设置为指定大小
+    file.set_len(image_size.try_into().unwrap())?;
+
+    // 写入全0数据
+    let zero_buffer = vec![0u8; 4096]; // 4KB buffer for writing zeros
+    let mut remaining_size = image_size;
+
+    while remaining_size > 0 {
+        let write_size = std::cmp::min(remaining_size, zero_buffer.len());
+        file.write_all(&zero_buffer[..write_size as usize])?;
+        remaining_size -= write_size;
+    }
+
+    Ok(())
+}
+
+struct DiskPartitioner;
+
+impl DiskPartitioner {
+    fn create_partitioned_image(disk_image_path: &PathBuf, part_type: PartitionType) -> Result<()> {
+        match part_type {
+            PartitionType::None => {
+                // This case should not be reached as we are in the partitioned image creation function
+                return Err(anyhow::anyhow!("Invalid partition type: None"));
+            }
+            PartitionType::Mbr => {
+                // Create MBR partitioned disk image
+                Self::create_mbr_partitioned_image(disk_image_path)?;
+            }
+            PartitionType::Gpt => {
+                // Create GPT partitioned disk image
+                Self::create_gpt_partitioned_image(disk_image_path)?;
+            }
+        }
+        Ok(())
+    }
+
+    fn create_mbr_partitioned_image(disk_image_path: &PathBuf) -> Result<()> {
+        let disk_image_path_str = disk_image_path.to_str().expect("Invalid path");
+
+        // 检查 fdisk 是否存在
+        let output = Command::new("fdisk")
+            .arg("--help")
+            .stdin(std::process::Stdio::piped())
+            .stdout(std::process::Stdio::piped())
+            .spawn()?
+            .wait_with_output()?;
+
+        if !output.status.success() {
+            return Err(anyhow::anyhow!("Command fdisk not found"));
+        }
+
+        // 向 fdisk 发送命令
+        let fdisk_commands = "o\nn\n\n\n\n\na\nw\n";
+        let mut fdisk_child = Command::new("fdisk")
+            .arg(disk_image_path_str)
+            .stdin(std::process::Stdio::piped())
+            .stdout(std::process::Stdio::piped())
+            .spawn()?;
+
+        let fdisk_stdin = fdisk_child.stdin.as_mut().expect("Failed to open stdin");
+        fdisk_stdin.write_all(fdisk_commands.as_bytes())?;
+        fdisk_stdin.flush()?;
+        fdisk_child
+            .wait()
+            .unwrap_or_else(|e| panic!("Failed to run fdisk: {}", e));
+        Ok(())
+    }
+
+    fn create_gpt_partitioned_image(_disk_image_path: &PathBuf) -> Result<()> {
+        // Implement the logic to create a GPT partitioned disk image
+        // This is a placeholder for the actual implementation
+        unimplemented!("Not implemented: create_gpt_partitioned_image");
+    }
+}
+
+struct DiskFormatter;
+
+impl DiskFormatter {
+    fn format_disk(disk_image_path: &PathBuf, fs_type: &FsType) -> Result<()> {
+        match fs_type {
+            FsType::Fat32 => Self::format_fat32(disk_image_path),
+        }
+    }
+
+    fn format_fat32(disk_image_path: &PathBuf) -> Result<()> {
+        // Use the `mkfs.fat` command to format the disk image as FAT32
+        let status = Command::new("mkfs.fat")
+            .arg("-F32")
+            .arg(disk_image_path.to_str().unwrap())
+            .status()?;
+
+        if status.success() {
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!("Failed to format disk image as FAT32"))
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs;
+    use std::io::Read;
+    use tempfile::NamedTempFile;
+
+    #[test]
+    fn test_create_raw_img_functional() -> Result<()> {
+        // 创建一个临时文件路径
+        let temp_file = NamedTempFile::new()?;
+        let disk_image_path = temp_file.path().to_path_buf();
+        let disk_image_size = 1024 * 1024usize;
+
+        // 调用函数
+        create_raw_img(&disk_image_path, disk_image_size)?;
+
+        // 验证文件大小
+        let metadata = fs::metadata(&disk_image_path)?;
+        assert_eq!(metadata.len(), disk_image_size as u64);
+
+        // 验证文件内容是否全为0
+        let mut file = File::open(&disk_image_path)?;
+        let mut buffer = vec![0u8; 4096];
+        let mut all_zeros = true;
+
+        while file.read(&mut buffer)? > 0 {
+            for byte in &buffer {
+                if *byte != 0 {
+                    all_zeros = false;
+                    break;
+                }
+            }
+        }
+
+        assert!(all_zeros, "File content is not all zeros");
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_format_fat32() {
+        // Create a temporary file to use as the disk image
+        let temp_file = NamedTempFile::new().expect("Failed to create temp file");
+        let disk_image_path = temp_file.path().to_path_buf();
+
+        // 16MB
+        let image_size = 16 * 1024 * 1024usize;
+        create_raw_img(&disk_image_path, image_size).expect("Failed to create raw disk image");
+
+        // Call the function to format the disk image
+        DiskFormatter::format_disk(&disk_image_path, &FsType::Fat32)
+            .expect("Failed to format disk image as FAT32");
+
+        // Optionally, you can check if the disk image was actually formatted as FAT32
+        // by running a command to inspect the filesystem type
+        let output = Command::new("file")
+            .arg("-sL")
+            .arg(&disk_image_path)
+            .output()
+            .expect("Failed to execute 'file' command");
+
+        let output_str = String::from_utf8_lossy(&output.stdout);
+        assert!(
+            output_str.contains("FAT (32 bit)"),
+            "Disk image is not formatted as FAT32"
+        );
+    }
+
+    #[test]
+    fn test_create_mbr_partitioned_image() -> Result<()> {
+        // Create a temporary file to use as the disk image
+        let temp_file = NamedTempFile::new()?;
+        let disk_image_path = temp_file.path().to_path_buf();
+
+        eprintln!("Disk image path: {:?}", disk_image_path);
+        // Create a raw disk image
+        let disk_image_size = 16 * 1024 * 1024usize; // 16MB
+        create_raw_img(&disk_image_path, disk_image_size)?;
+
+        // Call the function to create the MBR partitioned image
+        DiskPartitioner::create_mbr_partitioned_image(&disk_image_path)?;
+
+        // Verify the disk image has been correctly partitioned
+        let output = Command::new("fdisk")
+            .env("LANG", "C") // Set LANG to C to force English output
+            .env("LC_ALL", "C") // Set LC_ALL to C to force English output
+            .arg("-l")
+            .arg(&disk_image_path)
+            .output()
+            .expect("Failed to execute 'fdisk -l' command");
+
+        let output_str = String::from_utf8_lossy(&output.stdout);
+        assert!(
+            output_str.contains("Disklabel type: dos"),
+            "Disk image does not have an MBR partition table"
+        );
+        assert!(
+            output_str.contains("Start"),
+            "Disk image does not have a partition"
+        );
+
+        Ok(())
+    }
+}

+ 109 - 0
dadk/src/actions/rootfs/loopdev.rs

@@ -0,0 +1,109 @@
+use std::{path::PathBuf, process::Command};
+
+use anyhow::{anyhow, Result};
+
+pub struct LoopDevice {
+    img_path: PathBuf,
+    loop_device_path: Option<String>,
+}
+impl LoopDevice {
+    pub fn attach(&mut self) -> Result<()> {
+        if self.loop_device_path.is_some() {
+            return Ok(());
+        }
+        let output = Command::new("losetup")
+            .arg("-f")
+            .arg("--show")
+            .arg("-P")
+            .arg(&self.img_path)
+            .output()?;
+
+        if output.status.success() {
+            let loop_device = String::from_utf8(output.stdout)?.trim().to_string();
+            self.loop_device_path = Some(loop_device);
+            log::trace!(
+                "Loop device attached: {}",
+                self.loop_device_path.as_ref().unwrap()
+            );
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!(
+                "Failed to mount disk image: losetup command exited with status {}",
+                output.status
+            ))
+        }
+    }
+    /// 获取指定分区的路径
+    ///
+    /// # 参数
+    ///
+    /// * `nth` - 分区的编号
+    ///
+    /// # 返回值
+    ///
+    /// 返回一个 `Result<String>`,包含分区路径的字符串。如果循环设备未附加,则返回错误。
+    ///
+    /// # 错误
+    ///
+    /// 如果循环设备未附加,则返回 `anyhow!("Loop device not attached")` 错误。
+    pub fn partition_path(&self, nth: u8) -> Result<PathBuf> {
+        if self.loop_device_path.is_none() {
+            return Err(anyhow!("Loop device not attached"));
+        }
+        let s = format!("{}p{}", self.loop_device_path.as_ref().unwrap(), nth);
+        let s = PathBuf::from(s);
+        // 判断路径是否存在
+        if !s.exists() {
+            return Err(anyhow!("Partition not exist"));
+        }
+        Ok(s)
+    }
+
+    pub fn detach(&mut self) -> Result<()> {
+        if self.loop_device_path.is_none() {
+            return Ok(());
+        }
+        let loop_device = self.loop_device_path.take().unwrap();
+        let output = Command::new("losetup")
+            .arg("-d")
+            .arg(loop_device)
+            .output()?;
+
+        if output.status.success() {
+            self.loop_device_path = None;
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!("Failed to detach loop device"))
+        }
+    }
+}
+
+impl Drop for LoopDevice {
+    fn drop(&mut self) {
+        self.detach().expect("Failed to detach loop device");
+    }
+}
+
+pub struct LoopDeviceBuilder {
+    img_path: Option<PathBuf>,
+}
+
+impl LoopDeviceBuilder {
+    pub fn new() -> Self {
+        LoopDeviceBuilder { img_path: None }
+    }
+
+    pub fn img_path(mut self, img_path: PathBuf) -> Self {
+        self.img_path = Some(img_path);
+        self
+    }
+
+    pub fn build(self) -> Result<LoopDevice> {
+        let mut loop_dev = LoopDevice {
+            img_path: self.img_path.unwrap(),
+            loop_device_path: None,
+        };
+        loop_dev.attach()?;
+        Ok(loop_dev)
+    }
+}

+ 15 - 0
dadk/src/actions/rootfs/mod.rs

@@ -0,0 +1,15 @@
+use crate::{console::rootfs::RootFSCommand, context::DADKExecContext};
+use anyhow::Result;
+
+mod disk_img;
+mod loopdev;
+
+pub(super) fn run(ctx: &DADKExecContext, rootfs_cmd: &RootFSCommand) -> Result<()> {
+    match rootfs_cmd {
+        RootFSCommand::Create => disk_img::create(ctx),
+        RootFSCommand::Delete => todo!(),
+        RootFSCommand::DeleteSysroot => todo!(),
+        RootFSCommand::Mount => todo!(),
+        RootFSCommand::Unmount => todo!(),
+    }
+}

+ 3 - 0
dadk/src/console/mod.rs

@@ -30,9 +30,12 @@ pub struct CommandLineArgs {
 
 #[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
 pub enum Action {
+    /// 内核相关操作
     Kernel,
+    /// 对 rootfs 进行操作
     #[command(subcommand, name = "rootfs")]
     Rootfs(RootFSCommand),
+    /// 用户程序构建相关操作
     #[command(subcommand, name = "user")]
     User(UserCommand),
 }

+ 11 - 0
dadk/src/context/mod.rs

@@ -83,4 +83,15 @@ impl DADKExecContext {
     pub fn target_arch(&self) -> TargetArch {
         self.manifest.metadata.arch
     }
+
+    /// 获取磁盘镜像的路径,路径由工作目录、架构和固定文件名组成
+    pub fn disk_image_path(&self) -> PathBuf {
+        let arch: String = self.target_arch().into();
+        self.workdir().join(format!("bin/disk-image-{}.img", arch))
+    }
+
+    /// 获取磁盘镜像大小
+    pub fn disk_image_size(&self) -> usize {
+        self.rootfs().metadata.size
+    }
 }

+ 1 - 0
tests/data/.gitignore

@@ -1,2 +1,3 @@
 fake_dragonos_sysroot
 fake_dadk_cache_root
+/bin

+ 14 - 0
tests/data/config/rootfs.toml

@@ -0,0 +1,14 @@
+[metadata]
+# Filesystem type (options: `fat32`)
+fs_type = "fat32"
+# Size of the rootfs disk image (eg, `1G`, `1024M`)
+size = "16M"
+
+[partition]
+# Partition type (options: "none", "mbr", "gpt")
+#
+# If "none" is specified, no partition table will be created, 
+# and the entire disk will be treated as a single partition.
+#
+# Note that the "none" option is incompatible with GRUB boot.
+type = "mbr"