Skip to main content

flashkraft_core/domain/
constraints.rs

1//! Drive Constraints Module
2//!
3//! This module contains validation logic for checking drive/image compatibility
4//! based on various constraints similar to Etcher's constraint checking.
5
6use crate::domain::{DriveInfo, ImageInfo};
7
8/// The default unknown size for things such as images and drives
9const UNKNOWN_SIZE: f64 = 0.0;
10
11/// 128GB threshold for large drive warnings
12pub const LARGE_DRIVE_SIZE: f64 = 128.0;
13
14/// Compatibility status types
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum CompatibilityStatusType {
17    /// Warning status - user can still proceed but should be cautious
18    Warning,
19    /// Error status - drive should not be selectable
20    Error,
21}
22
23/// Compatibility status with type and message
24#[derive(Debug, Clone)]
25pub struct CompatibilityStatus {
26    /// Type of status (warning or error)
27    pub status_type: CompatibilityStatusType,
28    /// Human-readable message describing the issue
29    pub message: String,
30}
31
32impl CompatibilityStatus {
33    /// Create a new compatibility status
34    pub fn new(status_type: CompatibilityStatusType, message: String) -> Self {
35        Self {
36            status_type,
37            message,
38        }
39    }
40
41    /// Create an error status
42    pub fn error(message: String) -> Self {
43        Self::new(CompatibilityStatusType::Error, message)
44    }
45
46    /// Create a warning status
47    pub fn warning(message: String) -> Self {
48        Self::new(CompatibilityStatusType::Warning, message)
49    }
50}
51
52/// Check if a drive is a system drive
53///
54/// In the context of FlashKraft, a system drive is one that contains
55/// the operating system or critical system files.
56pub fn is_system_drive(drive: &DriveInfo) -> bool {
57    drive.is_system
58}
59
60/// Check if a drive is the source drive
61///
62/// A source drive is one that contains the image file being flashed.
63pub fn is_source_drive(drive: &DriveInfo, image: Option<&ImageInfo>) -> bool {
64    if let Some(img) = image {
65        // Check if the image path is inside any of the drive's mount points
66        let image_path = img.path.to_string_lossy();
67
68        // Simple check: if mount point is in the image path
69        if !drive.mount_point.is_empty()
70            && drive.mount_point != drive.device_path
71            && image_path.starts_with(&drive.mount_point)
72        {
73            return true;
74        }
75
76        // Also check if the device path matches
77        if image_path.starts_with(&drive.device_path) {
78            return true;
79        }
80    }
81    false
82}
83
84/// Check if a drive is large enough for an image
85///
86/// Returns true if the drive has sufficient space for the image,
87/// or if no image is provided (always valid).
88pub fn is_drive_large_enough(drive: &DriveInfo, image: Option<&ImageInfo>) -> bool {
89    let drive_size_gb = if drive.size_gb > 0.0 {
90        drive.size_gb
91    } else {
92        UNKNOWN_SIZE
93    };
94
95    if let Some(img) = image {
96        let image_size_gb = img.size_mb / 1024.0;
97
98        // If we don't know the drive size, assume it's not large enough
99        if drive_size_gb <= 0.0 {
100            return false;
101        }
102
103        // Drive must be at least as large as the image
104        return drive_size_gb >= image_size_gb;
105    }
106
107    // No image provided, so any drive is "large enough"
108    true
109}
110
111/// Check if a drive meets the recommended size
112///
113/// Some images may specify a recommended drive size larger than
114/// the image itself (for performance or other reasons).
115pub fn is_drive_size_recommended(_drive: &DriveInfo, _image: Option<&ImageInfo>) -> bool {
116    // For now, we don't have a recommendedDriveSize field in ImageInfo
117    // So we'll just return true. This can be extended later.
118    true
119}
120
121/// Check if a drive's size is considered "large"
122///
123/// Large drives (>128GB) trigger a warning to prevent accidental
124/// formatting of large storage devices.
125pub fn is_drive_size_large(drive: &DriveInfo) -> bool {
126    drive.size_gb > LARGE_DRIVE_SIZE
127}
128
129/// Check if a drive is valid for flashing
130///
131/// A drive is valid if it's not disabled, large enough for the image,
132/// and doesn't contain the source image.
133pub fn is_drive_valid(drive: &DriveInfo, image: Option<&ImageInfo>) -> bool {
134    !drive.disabled && is_drive_large_enough(drive, image) && !is_source_drive(drive, image)
135}
136
137/// Get all compatibility statuses for a drive/image pair
138///
139/// Returns a list of compatibility issues, which may be empty if
140/// the drive is fully compatible.
141pub fn get_drive_image_compatibility_statuses(
142    drive: &DriveInfo,
143    image: Option<&ImageInfo>,
144) -> Vec<CompatibilityStatus> {
145    let mut statuses = Vec::new();
146
147    // Check if drive is locked/read-only
148    if drive.is_read_only {
149        statuses.push(CompatibilityStatus::error(
150            "This drive is read-only and cannot be flashed.".to_string(),
151        ));
152    }
153
154    // Check if drive is too small
155    if !is_drive_large_enough(drive, image) {
156        if let Some(img) = image {
157            statuses.push(CompatibilityStatus::error(format!(
158                "Drive is too small. Need at least {:.2} GB, but drive is {:.2} GB.",
159                img.size_mb / 1024.0,
160                drive.size_gb
161            )));
162        } else {
163            statuses.push(CompatibilityStatus::error(
164                "Drive is too small for the image.".to_string(),
165            ));
166        }
167    } else {
168        // Only check these if drive is large enough
169
170        // Check if it's a system drive (warning)
171        if is_system_drive(drive) {
172            statuses.push(CompatibilityStatus::warning(
173                "This is a system drive. Flashing it may damage your operating system.".to_string(),
174            ));
175        } else if is_drive_size_large(drive) {
176            // Check if drive is very large (warning)
177            statuses.push(CompatibilityStatus::warning(format!(
178                "This drive is larger than {}GB. Are you sure this is the right drive?",
179                LARGE_DRIVE_SIZE as i32
180            )));
181        }
182
183        // Check if drive contains the source image
184        if is_source_drive(drive, image) {
185            statuses.push(CompatibilityStatus::error(
186                "This drive contains the source image and cannot be selected.".to_string(),
187            ));
188        }
189
190        // Check recommended size
191        if !is_drive_size_recommended(drive, image) {
192            statuses.push(CompatibilityStatus::warning(
193                "Drive size is smaller than recommended for optimal performance.".to_string(),
194            ));
195        }
196    }
197
198    statuses
199}
200
201/// Get compatibility statuses for a list of drives
202///
203/// Returns all compatibility statuses across all drives.
204pub fn get_list_drive_image_compatibility_statuses(
205    drives: &[DriveInfo],
206    image: Option<&ImageInfo>,
207) -> Vec<CompatibilityStatus> {
208    drives
209        .iter()
210        .flat_map(|drive| get_drive_image_compatibility_statuses(drive, image))
211        .collect()
212}
213
214/// Check if a drive has any compatibility issues
215///
216/// Returns true if there are any compatibility statuses (warnings or errors).
217pub fn has_drive_image_compatibility_status(drive: &DriveInfo, image: Option<&ImageInfo>) -> bool {
218    !get_drive_image_compatibility_statuses(drive, image).is_empty()
219}
220
221/// Mark drives as disabled based on compatibility checks
222///
223/// This updates the `disabled` field on drives that have error-level
224/// compatibility issues. Drives with only warnings remain enabled.
225pub fn mark_invalid_drives(drives: &mut [DriveInfo], image: Option<&ImageInfo>) {
226    for drive in drives.iter_mut() {
227        let statuses = get_drive_image_compatibility_statuses(drive, image);
228
229        // Disable if there are any error-level statuses
230        drive.disabled = statuses
231            .iter()
232            .any(|s| s.status_type == CompatibilityStatusType::Error);
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use std::path::PathBuf;
240
241    fn create_test_drive(size_gb: f64, is_system: bool, is_read_only: bool) -> DriveInfo {
242        DriveInfo::with_constraints(
243            "Test Drive".to_string(),
244            "/media/test".to_string(),
245            size_gb,
246            "/dev/sdb".to_string(),
247            is_system,
248            is_read_only,
249        )
250    }
251
252    fn create_test_image(size_mb: f64) -> ImageInfo {
253        ImageInfo {
254            path: PathBuf::from("/tmp/test.img"),
255            name: "test.img".to_string(),
256            size_mb,
257        }
258    }
259
260    #[test]
261    fn test_is_drive_large_enough() {
262        let drive = create_test_drive(16.0, false, false);
263        let image = create_test_image(8000.0); // 8GB
264
265        assert!(is_drive_large_enough(&drive, Some(&image)));
266    }
267
268    #[test]
269    fn test_is_drive_too_small() {
270        let drive = create_test_drive(4.0, false, false);
271        let image = create_test_image(8000.0); // 8GB
272
273        assert!(!is_drive_large_enough(&drive, Some(&image)));
274    }
275
276    #[test]
277    fn test_is_drive_size_large() {
278        let small_drive = create_test_drive(64.0, false, false);
279        let large_drive = create_test_drive(256.0, false, false);
280
281        assert!(!is_drive_size_large(&small_drive));
282        assert!(is_drive_size_large(&large_drive));
283    }
284
285    #[test]
286    fn test_system_drive_warning() {
287        let drive = create_test_drive(32.0, true, false);
288        let image = create_test_image(4000.0); // 4GB
289
290        let statuses = get_drive_image_compatibility_statuses(&drive, Some(&image));
291
292        assert!(!statuses.is_empty());
293        assert!(statuses
294            .iter()
295            .any(|s| s.status_type == CompatibilityStatusType::Warning));
296    }
297
298    #[test]
299    fn test_read_only_drive_error() {
300        let drive = create_test_drive(32.0, false, true);
301        let image = create_test_image(4000.0); // 4GB
302
303        let statuses = get_drive_image_compatibility_statuses(&drive, Some(&image));
304
305        assert!(!statuses.is_empty());
306        assert!(statuses
307            .iter()
308            .any(|s| s.status_type == CompatibilityStatusType::Error));
309    }
310
311    #[test]
312    fn test_mark_invalid_drives() {
313        let drives = vec![
314            create_test_drive(32.0, false, false), // Valid
315            create_test_drive(2.0, false, false),  // Too small
316            create_test_drive(16.0, false, true),  // Read-only
317        ];
318
319        let image = create_test_image(4000.0); // 4GB
320
321        let mut drives_mut = drives;
322        mark_invalid_drives(&mut drives_mut, Some(&image));
323
324        assert!(!drives_mut[0].disabled); // Valid drive
325        assert!(drives_mut[1].disabled); // Too small
326        assert!(drives_mut[2].disabled); // Read-only
327    }
328
329    #[test]
330    fn test_is_drive_valid() {
331        let mut valid_drive = create_test_drive(32.0, false, false);
332        let invalid_drive = create_test_drive(2.0, false, false);
333        let image = create_test_image(4000.0); // 4GB
334
335        assert!(is_drive_valid(&valid_drive, Some(&image)));
336        assert!(!is_drive_valid(&invalid_drive, Some(&image)));
337
338        // Test disabled flag
339        valid_drive.disabled = true;
340        assert!(!is_drive_valid(&valid_drive, Some(&image)));
341    }
342}