Use USB descriptors and MTP identity instead of the static libmtp product label. Monitor attachment, discard stale UI responses, clear disconnected views, and release slow MTP sessions in the background while preserving operation serialization. Publish 0.1.1 with immutable source history. Cover discovery, names, unplugging and slow disconnect with native and UI regressions. Verify real Pura 70 Ultra identity and prompt disconnect; run the newest published backend during Git-source checks.
312 lines
9.6 KiB
Rust
312 lines
9.6 KiB
Rust
use crate::model::{DeviceInfo, Entry, Fault, Progress, Result, Storage};
|
|
use std::{
|
|
ffi::{c_char, c_int, c_void, CStr, CString},
|
|
fs::File,
|
|
os::fd::AsRawFd,
|
|
ptr::NonNull,
|
|
sync::Once,
|
|
};
|
|
|
|
pub const ROOT: u32 = u32::MAX;
|
|
pub trait DeviceIo: Send {
|
|
fn storages(&mut self) -> Result<Vec<Storage>>;
|
|
fn list(&mut self, storage: u32, parent: u32) -> Result<Vec<Entry>>;
|
|
fn metadata(&mut self, object: u32) -> Result<Entry>;
|
|
fn upload(
|
|
&mut self,
|
|
file: &File,
|
|
name: &str,
|
|
size: u64,
|
|
storage: u32,
|
|
parent: u32,
|
|
progress: &Progress,
|
|
) -> Result<u32>;
|
|
fn download(&mut self, object: u32, file: &File, progress: &Progress) -> Result<()>;
|
|
fn mkdir(&mut self, name: &str, storage: u32, parent: u32) -> Result<u32>;
|
|
}
|
|
type DeviceCallback = extern "C" fn(*mut c_void, u32, u8, u16, u16, *const c_char, *const c_char);
|
|
type StorageCallback = extern "C" fn(*mut c_void, u32, *const c_char, u64, u64);
|
|
type FileCallback = extern "C" fn(*mut c_void, u32, u32, u32, *const c_char, u64, i64, c_int);
|
|
type ProgressCallback = extern "C" fn(u64, u64, *const c_void) -> c_int;
|
|
type IdentityCallback = extern "C" fn(*mut c_void, *const c_char, *const c_char, *const c_char);
|
|
extern "C" {
|
|
fn hc_init();
|
|
fn hc_version() -> *const c_char;
|
|
fn hc_scan(callback: DeviceCallback, context: *mut c_void) -> c_int;
|
|
fn hc_present(bus: u32, address: u8, vendor: u16, product: u16) -> c_int;
|
|
fn hc_identity(device: *mut c_void, callback: IdentityCallback, context: *mut c_void);
|
|
fn hc_open(bus: u32, address: u8) -> *mut c_void;
|
|
fn hc_close(device: *mut c_void);
|
|
fn hc_error(device: *mut c_void, buffer: *mut c_char, length: usize);
|
|
fn hc_storages(device: *mut c_void, callback: StorageCallback, context: *mut c_void) -> c_int;
|
|
fn hc_list(
|
|
device: *mut c_void,
|
|
storage: u32,
|
|
parent: u32,
|
|
callback: FileCallback,
|
|
context: *mut c_void,
|
|
) -> c_int;
|
|
fn hc_metadata(
|
|
device: *mut c_void,
|
|
object: u32,
|
|
callback: FileCallback,
|
|
context: *mut c_void,
|
|
) -> c_int;
|
|
fn hc_upload(
|
|
device: *mut c_void,
|
|
fd: c_int,
|
|
name: *const c_char,
|
|
size: u64,
|
|
storage: u32,
|
|
parent: u32,
|
|
callback: ProgressCallback,
|
|
context: *const c_void,
|
|
object: *mut u32,
|
|
) -> c_int;
|
|
fn hc_download(
|
|
device: *mut c_void,
|
|
object: u32,
|
|
fd: c_int,
|
|
callback: ProgressCallback,
|
|
context: *const c_void,
|
|
) -> c_int;
|
|
fn hc_mkdir(device: *mut c_void, name: *const c_char, storage: u32, parent: u32) -> u32;
|
|
}
|
|
fn initialize() {
|
|
static ONCE: Once = Once::new();
|
|
ONCE.call_once(|| unsafe { hc_init() });
|
|
}
|
|
fn text(value: *const c_char) -> String {
|
|
if value.is_null() {
|
|
return String::new();
|
|
}
|
|
unsafe { CStr::from_ptr(value) }
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
}
|
|
extern "C" fn device(
|
|
context: *mut c_void,
|
|
bus: u32,
|
|
address: u8,
|
|
vendor: u16,
|
|
product: u16,
|
|
manufacturer: *const c_char,
|
|
name: *const c_char,
|
|
) {
|
|
let rows = unsafe { &mut *(context as *mut Vec<DeviceInfo>) };
|
|
rows.push(DeviceInfo {
|
|
id: format!("{bus}:{address}"),
|
|
bus,
|
|
address,
|
|
vendor,
|
|
product,
|
|
manufacturer: text(manufacturer),
|
|
name: text(name),
|
|
model: None,
|
|
});
|
|
}
|
|
extern "C" fn identity(
|
|
context: *mut c_void,
|
|
friendly: *const c_char,
|
|
model: *const c_char,
|
|
manufacturer: *const c_char,
|
|
) {
|
|
let info = unsafe { &mut *(context as *mut DeviceInfo) };
|
|
let clean = |value| {
|
|
text(value)
|
|
.trim()
|
|
.chars()
|
|
.filter(|c| !c.is_control())
|
|
.collect::<String>()
|
|
};
|
|
let (friendly, model, manufacturer) = (clean(friendly), clean(model), clean(manufacturer));
|
|
if !model.is_empty() {
|
|
info.model = Some(model.clone());
|
|
}
|
|
if !manufacturer.is_empty() {
|
|
info.manufacturer = manufacturer;
|
|
}
|
|
if !friendly.is_empty() {
|
|
info.name = friendly;
|
|
} else if !model.is_empty() {
|
|
info.name = model;
|
|
}
|
|
}
|
|
// Presence checks enumerate descriptors only, on a separate libusb context; no MTP handle is accessed.
|
|
pub fn present(info: &DeviceInfo) -> Result<bool> {
|
|
initialize();
|
|
match unsafe { hc_present(info.bus, info.address, info.vendor, info.product) } {
|
|
0 => Ok(false),
|
|
1 => Ok(true),
|
|
_ => Err(Fault::new(
|
|
"usb.discovery_failed",
|
|
"暂时无法检查 USB 连接。",
|
|
)),
|
|
}
|
|
}
|
|
extern "C" fn storage(
|
|
context: *mut c_void,
|
|
id: u32,
|
|
name: *const c_char,
|
|
capacity: u64,
|
|
free: u64,
|
|
) {
|
|
unsafe { &mut *(context as *mut Vec<Storage>) }.push(Storage {
|
|
id,
|
|
name: text(name),
|
|
capacity,
|
|
free,
|
|
});
|
|
}
|
|
extern "C" fn entry(
|
|
context: *mut c_void,
|
|
id: u32,
|
|
parent: u32,
|
|
storage: u32,
|
|
name: *const c_char,
|
|
size: u64,
|
|
modified: i64,
|
|
folder: c_int,
|
|
) {
|
|
unsafe { &mut *(context as *mut Vec<Entry>) }.push(Entry {
|
|
id,
|
|
parent,
|
|
storage,
|
|
name: text(name),
|
|
size,
|
|
modified,
|
|
folder: folder != 0,
|
|
});
|
|
}
|
|
extern "C" fn progress(sent: u64, total: u64, context: *const c_void) -> c_int {
|
|
let progress = unsafe { &*(context as *const Progress) };
|
|
progress.update(sent, total);
|
|
i32::from(progress.cancelled())
|
|
}
|
|
pub fn version() -> String {
|
|
initialize();
|
|
text(unsafe { hc_version() })
|
|
}
|
|
// Caller holds the connector operation gate: libmtp enumeration and sessions never run concurrently.
|
|
pub fn scan() -> Result<Vec<DeviceInfo>> {
|
|
initialize();
|
|
let mut rows = Vec::new();
|
|
let status = unsafe { hc_scan(device, &mut rows as *mut _ as *mut c_void) };
|
|
if status != 0 {
|
|
return Err(Fault::new(
|
|
"mtp.discovery_failed",
|
|
"无法扫描 USB 设备。请检查数据线、手机授权及其他文件传输应用是否正在占用设备。",
|
|
));
|
|
}
|
|
Ok(rows)
|
|
}
|
|
pub struct NativeDevice(NonNull<c_void>);
|
|
// The C handle has one owner. The connector gate and session mutex serialize every access.
|
|
unsafe impl Send for NativeDevice {}
|
|
impl NativeDevice {
|
|
pub fn identify(&mut self, info: &mut DeviceInfo) {
|
|
unsafe { hc_identity(self.0.as_ptr(), identity, info as *mut _ as *mut c_void) };
|
|
}
|
|
pub fn open(device: &DeviceInfo) -> Result<Self> {
|
|
initialize();
|
|
NonNull::new(unsafe { hc_open(device.bus, device.address) }).map(Self)
|
|
.ok_or_else(|| Fault::new("mtp.connection_failed", "无法打开设备。请解锁手机、选择“传输文件”,并退出正在占用设备的 HiSuite、OpenMTP 或其他传输应用。"))
|
|
}
|
|
fn checked(&self, code: c_int) -> Result<()> {
|
|
if code == 0 {
|
|
return Ok(());
|
|
}
|
|
let mut buffer = [0i8; 1024];
|
|
unsafe { hc_error(self.0.as_ptr(), buffer.as_mut_ptr(), buffer.len()) };
|
|
Err(Fault::new(
|
|
"mtp.operation_failed",
|
|
format!("设备未完成操作:{}", text(buffer.as_ptr())),
|
|
))
|
|
}
|
|
}
|
|
impl Drop for NativeDevice {
|
|
fn drop(&mut self) {
|
|
unsafe { hc_close(self.0.as_ptr()) };
|
|
}
|
|
}
|
|
impl DeviceIo for NativeDevice {
|
|
fn storages(&mut self) -> Result<Vec<Storage>> {
|
|
let mut rows = Vec::new();
|
|
self.checked(unsafe {
|
|
hc_storages(self.0.as_ptr(), storage, &mut rows as *mut _ as *mut c_void)
|
|
})?;
|
|
Ok(rows)
|
|
}
|
|
fn list(&mut self, storage: u32, parent: u32) -> Result<Vec<Entry>> {
|
|
let mut rows = Vec::new();
|
|
self.checked(unsafe {
|
|
hc_list(
|
|
self.0.as_ptr(),
|
|
storage,
|
|
parent,
|
|
entry,
|
|
&mut rows as *mut _ as *mut c_void,
|
|
)
|
|
})?;
|
|
Ok(rows)
|
|
}
|
|
fn metadata(&mut self, object: u32) -> Result<Entry> {
|
|
let mut rows = Vec::new();
|
|
self.checked(unsafe {
|
|
hc_metadata(
|
|
self.0.as_ptr(),
|
|
object,
|
|
entry,
|
|
&mut rows as *mut _ as *mut c_void,
|
|
)
|
|
})?;
|
|
rows.pop()
|
|
.ok_or_else(|| Fault::new("mtp.not_found", "该文件已不存在,请刷新列表。"))
|
|
}
|
|
fn upload(
|
|
&mut self,
|
|
file: &File,
|
|
name: &str,
|
|
size: u64,
|
|
storage: u32,
|
|
parent: u32,
|
|
state: &Progress,
|
|
) -> Result<u32> {
|
|
let name = CString::new(name)
|
|
.map_err(|_| Fault::new("file.invalid_name", "文件名包含无效字符。"))?;
|
|
let mut object = 0;
|
|
self.checked(unsafe {
|
|
hc_upload(
|
|
self.0.as_ptr(),
|
|
file.as_raw_fd(),
|
|
name.as_ptr(),
|
|
size,
|
|
storage,
|
|
parent,
|
|
progress,
|
|
state as *const _ as *const c_void,
|
|
&mut object,
|
|
)
|
|
})?;
|
|
Ok(object)
|
|
}
|
|
fn download(&mut self, object: u32, file: &File, state: &Progress) -> Result<()> {
|
|
self.checked(unsafe {
|
|
hc_download(
|
|
self.0.as_ptr(),
|
|
object,
|
|
file.as_raw_fd(),
|
|
progress,
|
|
state as *const _ as *const c_void,
|
|
)
|
|
})
|
|
}
|
|
fn mkdir(&mut self, name: &str, storage: u32, parent: u32) -> Result<u32> {
|
|
let name = CString::new(name)
|
|
.map_err(|_| Fault::new("file.invalid_name", "文件夹名称包含无效字符。"))?;
|
|
let id = unsafe { hc_mkdir(self.0.as_ptr(), name.as_ptr(), storage, parent) };
|
|
self.checked(if id == 0 { -1 } else { 0 })?;
|
|
Ok(id)
|
|
}
|
|
}
|