zinit 0.3.6

Process supervisor with dependency management
Documentation
//! Empty disk verification - ensures a disk is safe to initialize

use super::cmd;
use super::detect::{BlockDevice, has_holders, has_partitions, is_mounted};
use super::error::StorageError;

/// Result of checking if a disk is empty
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EmptyCheckResult {
    /// Disk is empty and safe to initialize
    Empty,
    /// Disk has a partition table
    HasPartitionTable,
    /// Disk has filesystem signatures
    HasFilesystem,
    /// Disk has partitions
    HasPartitions,
    /// Disk is mounted
    IsMounted,
    /// Disk is in use by device mapper or similar
    HasHolders,
}

impl EmptyCheckResult {
    /// Check if the result indicates the disk is empty
    pub fn is_empty(&self) -> bool {
        matches!(self, EmptyCheckResult::Empty)
    }
}

/// Verify that a disk is completely empty and safe to initialize
///
/// Performs multiple checks:
/// 1. No partitions exist in sysfs
/// 2. Not currently mounted
/// 3. No holders (device mapper, etc.)
/// 4. blkid returns nothing (no filesystem signature)
/// 5. sgdisk shows no existing partitions
pub fn verify_empty(device: &BlockDevice) -> Result<EmptyCheckResult, StorageError> {
    let dev_path = device.path.to_string_lossy();
    log::debug!("verifying device is empty: {}", dev_path);

    // Check 1: No partitions in sysfs
    if has_partitions(device) {
        log::debug!("{}: has partitions in sysfs", dev_path);
        return Ok(EmptyCheckResult::HasPartitions);
    }

    // Check 2: Not mounted
    if is_mounted(device) {
        log::debug!("{}: is mounted", dev_path);
        return Ok(EmptyCheckResult::IsMounted);
    }

    // Check 3: No holders
    if has_holders(device) {
        log::debug!("{}: has holders", dev_path);
        return Ok(EmptyCheckResult::HasHolders);
    }

    // Check 4: blkid returns nothing
    if has_blkid_signature(device)? {
        log::debug!("{}: has blkid signature", dev_path);
        return Ok(EmptyCheckResult::HasFilesystem);
    }

    // Check 5: sgdisk shows no partitions
    if has_sgdisk_partitions(device)? {
        log::debug!("{}: has sgdisk partitions", dev_path);
        return Ok(EmptyCheckResult::HasPartitionTable);
    }

    log::info!("{}: verified empty", dev_path);
    Ok(EmptyCheckResult::Empty)
}

/// Check if blkid reports any signature on the device
fn has_blkid_signature(device: &BlockDevice) -> Result<bool, StorageError> {
    let dev_path = device.path.to_string_lossy();

    // blkid -p returns nothing for empty devices
    let output = cmd::run_allow_fail("blkid", &["-p", &dev_path]);

    match output {
        Some(o) => {
            // If blkid outputs anything, there's a signature
            let has_output = !o.stdout.is_empty();
            Ok(has_output)
        }
        None => {
            // Command failed to run - assume safe (no blkid)
            log::warn!("blkid command failed, assuming no signature");
            Ok(false)
        }
    }
}

/// Check if sgdisk reports any partitions
fn has_sgdisk_partitions(device: &BlockDevice) -> Result<bool, StorageError> {
    let dev_path = device.path.to_string_lossy();

    let output = cmd::run_allow_fail("sgdisk", &["-p", &dev_path]);

    match output {
        Some(o) => {
            let stdout = String::from_utf8_lossy(&o.stdout);

            // If sgdisk succeeds and shows partition info, check partition count
            // Look for "Number  Start" header followed by partition lines
            // or check if output contains "Creating new GPT entries" which means no existing GPT

            if stdout.contains("Creating new GPT entries") {
                // No existing partition table
                return Ok(false);
            }

            // Check for partition lines (format: "   1   2048   ...")
            for line in stdout.lines() {
                let trimmed = line.trim();
                // Partition lines start with a number
                if let Some(first_char) = trimmed.chars().next()
                    && first_char.is_ascii_digit()
                    && trimmed.contains("   ")
                {
                    // This looks like a partition entry
                    return Ok(true);
                }
            }

            Ok(false)
        }
        None => {
            // sgdisk failed - this often means no GPT
            Ok(false)
        }
    }
}

/// Find the first empty disk from a list of candidates
pub fn find_first_empty(devices: &[BlockDevice]) -> Result<Option<&BlockDevice>, StorageError> {
    for device in devices {
        match verify_empty(device)? {
            EmptyCheckResult::Empty => {
                log::info!("found empty disk: {}", device.name);
                return Ok(Some(device));
            }
            reason => {
                log::debug!("skipping {}: {:?}", device.name, reason);
            }
        }
    }

    log::info!("no empty disks found");
    Ok(None)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_empty_check_result_is_empty() {
        assert!(EmptyCheckResult::Empty.is_empty());
        assert!(!EmptyCheckResult::HasPartitions.is_empty());
        assert!(!EmptyCheckResult::HasFilesystem.is_empty());
    }
}