aster_forge_cloud_files_windows/
progress.rs

1//! Product-neutral CFAPI provider progress validation.
2
3use std::cmp::Ordering;
4
5use crate::{Result, WindowsCloudFilesError};
6
7/// One provider progress sample for a pending CFAPI transfer.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct WindowsFetchDataProgress {
10    total: u64,
11    completed: u64,
12}
13
14impl WindowsFetchDataProgress {
15    /// Creates a progress sample. Zero-sized work is represented as `(0, 0)`.
16    /// # Errors
17    ///
18    /// Returns an error when validation fails or an underlying backend, store, or platform
19    /// operation fails.
20    pub fn new(total: u64, completed: u64) -> Result<Self> {
21        if completed > total {
22            return Err(WindowsCloudFilesError::InvalidProviderProgress {
23                reason: "completed bytes exceed total bytes",
24            });
25        }
26        if total > i64::MAX as u64 {
27            return Err(WindowsCloudFilesError::InvalidProviderProgress {
28                reason: "total bytes exceed signed CFAPI boundary",
29            });
30        }
31        if completed > i64::MAX as u64 {
32            return Err(WindowsCloudFilesError::InvalidProviderProgress {
33                reason: "completed bytes exceed signed CFAPI boundary",
34            });
35        }
36        Ok(Self { total, completed })
37    }
38
39    /// Returns the total number of bytes in the provider operation.
40    #[must_use]
41    pub const fn total(self) -> u64 {
42        self.total
43    }
44
45    /// Returns the number of bytes completed so far.
46    #[must_use]
47    pub const fn completed(self) -> u64 {
48        self.completed
49    }
50
51    /// Returns the signed values accepted by `CfReportProviderProgress`.
52    #[must_use]
53    pub const fn as_cfapi(self) -> (i64, i64) {
54        (self.total.cast_signed(), self.completed.cast_signed())
55    }
56
57    /// Validates a new sample against a previous sample for the same transfer.
58    /// # Errors
59    ///
60    /// Returns an error when validation fails or an underlying backend, store, or platform
61    /// operation fails.
62    pub fn advance_from(self, previous: Self) -> Result<Self> {
63        if self.total != previous.total {
64            return Err(WindowsCloudFilesError::InvalidProviderProgress {
65                reason: "progress total changed during one transfer",
66            });
67        }
68        match self.completed.cmp(&previous.completed) {
69            Ordering::Less => Err(WindowsCloudFilesError::InvalidProviderProgress {
70                reason: "completed bytes regressed during one transfer",
71            }),
72            Ordering::Equal | Ordering::Greater => Ok(self),
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn progress_accepts_zero_and_exact_completion() {
83        assert_eq!(
84            WindowsFetchDataProgress::new(0, 0).unwrap().as_cfapi(),
85            (0, 0)
86        );
87        assert_eq!(
88            WindowsFetchDataProgress::new(i64::MAX as u64, i64::MAX as u64)
89                .unwrap()
90                .as_cfapi(),
91            (i64::MAX, i64::MAX)
92        );
93    }
94
95    #[test]
96    fn progress_rejects_overflow_and_regression() {
97        assert!(matches!(
98            WindowsFetchDataProgress::new(1, 2),
99            Err(WindowsCloudFilesError::InvalidProviderProgress { .. })
100        ));
101        assert!(matches!(
102            WindowsFetchDataProgress::new(i64::MAX as u64 + 1, 0),
103            Err(WindowsCloudFilesError::InvalidProviderProgress { .. })
104        ));
105        let previous = WindowsFetchDataProgress::new(10, 5).unwrap();
106        let next = WindowsFetchDataProgress::new(10, 4).unwrap();
107        assert!(matches!(
108            next.advance_from(previous),
109            Err(WindowsCloudFilesError::InvalidProviderProgress { .. })
110        ));
111        let changed_total = WindowsFetchDataProgress::new(11, 5).unwrap();
112        assert!(changed_total.advance_from(previous).is_err());
113    }
114}