cadmus_core/device/usb/kobo/
legacy.rs

1//! Legacy USB gadget implementation using kernel module loading.
2//!
3//! This module provides USB mass storage support for older Kobo devices that
4//! use the traditional kernel module approach (`g_mass_storage` or
5//! `g_file_storage`). It handles the platform-specific module loading and
6//! unloading sequences.
7
8use crate::device::metadata::{DeviceMetadata, Platform};
9use crate::device::usb::error::UsbError;
10use crate::device::usb::kobo::operations::KoboUsbOperations;
11use crate::device::usb::manager::UsbManager;
12use std::fs;
13use std::path::Path;
14use std::process::Command;
15use tracing::{debug, error, info, warn};
16
17/// Base path for platform-specific kernel modules.
18const DRIVERS_DIR: &str = "/drivers";
19
20/// SD card block device partition, present only when a card is inserted.
21const SD_CARD_PARTITION: &str = "/dev/mmcblk1p1";
22
23/// USB mass storage manager for legacy platforms.
24///
25/// This implementation loads kernel modules to enable USB mass storage on
26/// older Kobo devices. For each platform it first tries `g_mass_storage.ko`;
27/// if that is absent it falls back to `g_file_storage.ko` with
28/// platform-specific dependencies:
29///
30/// - **mx6sll-ntx, mx6ull-ntx**: loads configfs, libcomposite, usb_f_mass_storage deps
31/// - **mx6sl-ntx**: no extra dependencies (arcotg_udc is built into the kernel)
32/// - **other platforms**: loads arcotg_udc before g_file_storage
33pub struct LegacyUsbManager {
34    metadata: DeviceMetadata,
35    platform: Platform,
36}
37
38impl LegacyUsbManager {
39    /// Creates a new legacy USB manager.
40    ///
41    /// Accepts the platform detected by the caller. No USB operations
42    /// are performed until [`enable`](UsbManager::enable) is called.
43    pub fn new(metadata: DeviceMetadata, platform: Platform) -> Self {
44        Self { metadata, platform }
45    }
46
47    fn drivers_path(&self) -> String {
48        format!("{}/{}", DRIVERS_DIR, self.platform)
49    }
50
51    fn has_g_mass_storage(&self) -> bool {
52        let path = format!("{}/g_mass_storage.ko", self.drivers_path());
53        Path::new(&path).exists()
54    }
55
56    /// Builds the `file=` parameter value for `insmod`, including the SD card
57    /// partition when one is present (matching the original `usb-enable.sh` behavior).
58    fn build_file_param(&self) -> String {
59        if Path::new(SD_CARD_PARTITION).exists() {
60            debug!(
61                sd_partition = SD_CARD_PARTITION,
62                "SD card detected, including in USB export"
63            );
64            format!("{},{}", self.metadata.partition, SD_CARD_PARTITION)
65        } else {
66            self.metadata.partition.clone()
67        }
68    }
69
70    fn build_mass_storage_params(&self) -> Vec<String> {
71        vec![
72            format!("idVendor=0x{:04X}", self.metadata.vendor_id),
73            format!("idProduct=0x{:04X}", self.metadata.product_id),
74            "iManufacturer=Kobo".to_string(),
75            format!("iProduct=eReader-{}", self.metadata.firmware_version),
76            format!("iSerialNumber={}", self.metadata.serial_number),
77            format!("file={}", self.build_file_param()),
78            "stall=1".to_string(),
79            "removable=1".to_string(),
80        ]
81    }
82
83    fn build_file_storage_params(&self) -> Vec<String> {
84        match self.platform {
85            Platform::MX6SLLNTX | Platform::MX6ULLNTX => self.build_mass_storage_params(),
86            _ => {
87                vec![
88                    format!("vendor=0x{:04X}", self.metadata.vendor_id),
89                    format!("product=0x{:04X}", self.metadata.product_id),
90                    "vendor_id=Kobo".to_string(),
91                    format!("product_id=eReader-{}", self.metadata.firmware_version),
92                    format!("SN={}", self.metadata.serial_number),
93                    format!("file={}", self.build_file_param()),
94                    "stall=1".to_string(),
95                    "removable=1".to_string(),
96                ]
97            }
98        }
99    }
100
101    fn load_g_mass_storage(&self) -> Result<(), UsbError> {
102        info!("Loading g_mass_storage module");
103
104        let module_path = format!("{}/g_mass_storage.ko", self.drivers_path());
105        let params = self.build_mass_storage_params();
106
107        let mut cmd = Command::new("insmod");
108        cmd.arg(&module_path);
109        for param in &params {
110            cmd.arg(param);
111        }
112
113        let output = cmd.output().map_err(|e| {
114            error!(error = %e, "Failed to execute insmod");
115            UsbError::KernelModule(format!("insmod execution failed: {}", e))
116        })?;
117
118        if !output.status.success() {
119            let stderr = String::from_utf8_lossy(&output.stderr);
120            error!(stderr = %stderr, "insmod failed");
121            return Err(UsbError::KernelModule(format!(
122                "Failed to load g_mass_storage: {}",
123                stderr
124            )));
125        }
126
127        info!("g_mass_storage module loaded successfully");
128        Ok(())
129    }
130
131    fn load_g_file_storage(&self) -> Result<(), UsbError> {
132        info!("Loading g_file_storage module with dependencies");
133
134        let gadgets_path = format!("{}/{}/usb/gadget", DRIVERS_DIR, self.platform);
135
136        match self.platform {
137            Platform::MX6SLLNTX | Platform::MX6ULLNTX => {
138                for module in ["configfs.ko", "libcomposite.ko", "usb_f_mass_storage.ko"] {
139                    let path = format!("{}/{}", gadgets_path, module);
140                    if Path::new(&path).exists() {
141                        debug!(module = %module, "Loading dependency module");
142                        let _ = Command::new("insmod").arg(&path).output();
143                    }
144                }
145            }
146            Platform::MX6SLNTX => {}
147            _ => {
148                let arcotg_path = format!("{}/arcotg_udc.ko", gadgets_path);
149                if Path::new(&arcotg_path).exists() {
150                    debug!("Loading arcotg_udc module");
151                    let _ = Command::new("insmod").arg(&arcotg_path).output();
152                    std::thread::sleep(std::time::Duration::from_secs(2));
153                }
154            }
155        }
156
157        let module_path = format!("{}/g_file_storage.ko", gadgets_path);
158        let params = self.build_file_storage_params();
159
160        let mut cmd = Command::new("insmod");
161        cmd.arg(&module_path);
162        for param in &params {
163            cmd.arg(param);
164        }
165
166        let output = cmd.output().map_err(|e| {
167            error!(error = %e, "Failed to execute insmod");
168            UsbError::KernelModule(format!("insmod execution failed: {}", e))
169        })?;
170
171        if !output.status.success() {
172            let stderr = String::from_utf8_lossy(&output.stderr);
173            error!(stderr = %stderr, "insmod failed");
174            return Err(UsbError::KernelModule(format!(
175                "Failed to load g_file_storage: {}",
176                stderr
177            )));
178        }
179
180        info!("g_file_storage module loaded successfully");
181        Ok(())
182    }
183
184    fn load_usb_module(&self) -> Result<(), UsbError> {
185        if self.has_g_mass_storage() {
186            self.load_g_mass_storage()
187        } else {
188            self.load_g_file_storage()
189        }
190    }
191
192    fn get_loaded_module(&self) -> Option<String> {
193        let content = fs::read_to_string("/proc/modules").ok()?;
194
195        for line in content.lines() {
196            if line.starts_with("g_mass_storage ") {
197                return Some("g_mass_storage".to_string());
198            }
199            if line.starts_with("g_file_storage ") {
200                return Some("g_file_storage".to_string());
201            }
202        }
203
204        None
205    }
206
207    fn unload_usb_modules(&self) -> Result<(), UsbError> {
208        let module = match self.get_loaded_module() {
209            Some(m) => m,
210            None => {
211                warn!("No USB module found in /proc/modules");
212                return Ok(());
213            }
214        };
215
216        info!(module = %module, "Unloading USB module");
217
218        let output = Command::new("rmmod").arg(&module).output().map_err(|e| {
219            error!(error = %e, "Failed to execute rmmod");
220            UsbError::KernelModule(format!("rmmod execution failed: {}", e))
221        })?;
222
223        if !output.status.success() {
224            let stderr = String::from_utf8_lossy(&output.stderr);
225            error!(stderr = %stderr, "rmmod failed");
226            return Err(UsbError::KernelModule(format!(
227                "Failed to unload {}: {}",
228                module, stderr
229            )));
230        }
231
232        if module == "g_file_storage" {
233            match self.platform {
234                Platform::MX6SLLNTX | Platform::MX6ULLNTX => {
235                    for mod_name in ["usb_f_mass_storage", "libcomposite", "configfs"] {
236                        let _ = Command::new("rmmod").arg(mod_name).output();
237                    }
238                }
239                Platform::MX6SLNTX => {}
240                _ => {
241                    let _ = Command::new("rmmod").arg("arcotg_udc").output();
242                }
243            }
244        }
245
246        info!("USB modules unloaded successfully");
247        Ok(())
248    }
249}
250
251impl KoboUsbOperations for LegacyUsbManager {
252    fn metadata(&self) -> &DeviceMetadata {
253        &self.metadata
254    }
255}
256
257impl UsbManager for LegacyUsbManager {
258    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
259    fn enable(&self) -> Result<(), UsbError> {
260        info!(platform = %self.platform, "Enabling legacy USB mass storage");
261
262        self.prepare_for_usb_share()?;
263        self.load_usb_module()?;
264
265        std::thread::sleep(std::time::Duration::from_secs(1));
266
267        info!("Legacy USB mass storage enabled successfully");
268        Ok(())
269    }
270
271    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
272    fn disable(&self) -> Result<(), UsbError> {
273        info!(platform = %self.platform, "Disabling legacy USB mass storage");
274
275        self.unload_usb_modules()?;
276
277        std::thread::sleep(std::time::Duration::from_secs(1));
278
279        self.check_filesystem()?;
280        self.remount_partitions()?;
281
282        info!("Legacy USB mass storage disabled successfully");
283        Ok(())
284    }
285}