Skip to main content

marlin_binary_transfer/adapters/
common.rs

1//! Shared types used by both the [`blocking`](crate::adapters::blocking) and
2//! [`tokio`](crate::adapters::tokio) adapter modules.
3//!
4//! Living here rather than in either adapter module so that enabling one
5//! adapter doesn't require enabling the other to resolve `UploadOptions` /
6//! `UploadStats` / `UploadError` symbols.
7
8use thiserror::Error;
9
10use crate::file_transfer::{Compression, FileError};
11
12/// Conservative fallback when the device-advertised block size is zero
13/// (which shouldn't happen after a successful SYNC handshake but we
14/// handle it defensively).
15const FALLBACK_CHUNK_SIZE: usize = 256;
16
17/// Resolve the per-WRITE chunk size from caller options and the device
18/// max.
19///
20/// - `requested == 0` means "use the device-advertised max verbatim".
21/// - `requested > 0` is honored, but capped to the device max so a
22///   caller asking for 4096 against a device that says 512 doesn't
23///   blow past what the device can buffer.
24/// - `device_max == 0` falls back to [`FALLBACK_CHUNK_SIZE`].
25pub(crate) fn resolve_chunk_size(requested: usize, device_max: u16) -> usize {
26    let device_max = if device_max == 0 {
27        FALLBACK_CHUNK_SIZE
28    } else {
29        device_max as usize
30    };
31    if requested == 0 {
32        device_max
33    } else {
34        requested.min(device_max)
35    }
36}
37
38/// Caller-supplied options controlling the upload.
39#[derive(Debug, Clone)]
40pub struct UploadOptions {
41    /// Destination filename on the device's SD card. Required.
42    pub dest_filename: String,
43    /// Compression preference. Defaults to [`Compression::None`].
44    pub compression: Compression,
45    /// Set to true to make the device pretend to receive a file without
46    /// actually writing it; useful for protocol smoke tests.
47    pub dummy: bool,
48    /// Bytes per WRITE packet. Capped to the device-advertised maximum
49    /// after the SYNC handshake completes. `0` means "use the device's
50    /// max_block_size verbatim".
51    pub chunk_size: usize,
52}
53
54impl Default for UploadOptions {
55    fn default() -> Self {
56        Self {
57            dest_filename: String::new(),
58            compression: Compression::None,
59            dummy: false,
60            chunk_size: 0,
61        }
62    }
63}
64
65/// Upload statistics returned on success.
66#[derive(Debug, Clone, Default)]
67pub struct UploadStats {
68    /// Bytes read from `src`.
69    pub source_bytes: u64,
70    /// Bytes written across all WRITE packets (post-compression).
71    pub bytes_sent: u64,
72    /// Number of WRITE packets.
73    pub chunks_sent: u64,
74    /// Compression actually used (resolved from [`Compression::Auto`]).
75    pub compression: Compression,
76}
77
78/// Errors the adapter upload helpers can produce.
79#[derive(Debug, Error)]
80pub enum UploadError {
81    /// Wrapping I/O error from the transport.
82    #[error("transport I/O error: {0}")]
83    Io(#[from] std::io::Error),
84    /// Underlying file-transfer state machine reported a failure.
85    #[error("file transfer failed: {0}")]
86    Transfer(#[from] FileError),
87    /// Reached an unrecoverable protocol state (e.g. device returned
88    /// nothing for too long with no progress).
89    #[error("upload stalled: {0}")]
90    Stalled(&'static str),
91    /// The session never completed the SYNC handshake before the helper
92    /// gave up.
93    #[error("SYNC handshake did not complete")]
94    HandshakeFailed,
95    /// Compression was requested but the `heatshrink` feature is not
96    /// enabled at compile time.
97    #[cfg(not(feature = "heatshrink"))]
98    #[error("heatshrink compression requested but the `heatshrink` feature is disabled")]
99    CompressionFeatureDisabled,
100    /// Heatshrink compression error.
101    #[cfg(feature = "heatshrink")]
102    #[error("heatshrink error: {0}")]
103    Heatshrink(#[from] crate::compression::HeatshrinkError),
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn requested_zero_uses_device_max() {
112        assert_eq!(resolve_chunk_size(0, 512), 512);
113    }
114
115    #[test]
116    fn requested_under_max_is_honored() {
117        assert_eq!(resolve_chunk_size(128, 512), 128);
118    }
119
120    #[test]
121    fn requested_over_max_is_capped() {
122        assert_eq!(resolve_chunk_size(4096, 512), 512);
123    }
124
125    #[test]
126    fn requested_zero_with_zero_device_max_falls_back() {
127        assert_eq!(resolve_chunk_size(0, 0), FALLBACK_CHUNK_SIZE);
128    }
129
130    #[test]
131    fn requested_nonzero_with_zero_device_max_capped_to_fallback() {
132        assert_eq!(resolve_chunk_size(1024, 0), FALLBACK_CHUNK_SIZE);
133    }
134
135    #[test]
136    fn requested_equal_to_max_is_honored() {
137        assert_eq!(resolve_chunk_size(512, 512), 512);
138    }
139}