Skip to main content

mtp_rs/mtp/
types.rs

1//! Backend-neutral types for the high-level [`crate::mtp`] API.
2//!
3//! These types are deliberately independent of any single backend's wire format. The PTP-over-USB
4//! backend ([`crate::ptp`]) and the Windows WPD-over-COM backend both produce and consume them,
5//! converting from their own representations at the boundary. See
6//! `docs/windows-wpd-backend-plan.md` for the design.
7//!
8//! Where a value originates in the PTP layer, a `From` impl (and a `pub(crate)` `to_ptp` helper for
9//! the reverse) bridges the two so the `UsbBackend` converts only at its edge.
10
11use crate::ptp::{
12    AccessCapability, DateTime as PtpDateTime, DeviceInfo as PtpDeviceInfo,
13    FilesystemType as PtpFs, ObjectFormatCode, ObjectHandle as PtpObjectHandle,
14    ObjectInfo as PtpObjectInfo, OperationCode, StorageId as PtpStorageId,
15    StorageInfo as PtpStorageInfo, StorageType as PtpStorageType,
16};
17
18/// Opaque handle for an object on a device.
19///
20/// The inner value is a backend-defined token, meaningful only within a single open device session
21/// — **not** a stable, cross-session, or wire-level identifier. The USB/PTP backend uses the raw PTP
22/// object handle; the WPD backend maps the token to an opaque WPD object-ID string. Treat it as
23/// opaque: pass it back to the same open device, don't persist it across sessions, and don't derive
24/// meaning from its numeric value. (Some devices, notably Android, re-key handles even within a
25/// session — see [`crate::Error::StaleHandle`].)
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
27pub struct ObjectHandle(pub u64);
28
29impl ObjectHandle {
30    /// The storage root (an object's parent is `ROOT` when it sits at the top of a storage).
31    pub const ROOT: Self = ObjectHandle(0x0000_0000);
32    /// Sentinel meaning "all objects" for recursive listing.
33    pub const ALL: Self = ObjectHandle(0xFFFF_FFFF);
34
35    /// Narrow to the PTP wire handle. Sound for the USB backend because its tokens originate as
36    /// widened `u32` PTP handles; `ROOT`/`ALL` round-trip exactly.
37    #[must_use]
38    pub(crate) fn to_ptp(self) -> PtpObjectHandle {
39        PtpObjectHandle(self.0 as u32)
40    }
41}
42
43impl From<PtpObjectHandle> for ObjectHandle {
44    fn from(h: PtpObjectHandle) -> Self {
45        ObjectHandle(u64::from(h.0))
46    }
47}
48
49/// Opaque identifier for a storage on a device.
50///
51/// Like [`ObjectHandle`], this is a backend-defined token. The USB/PTP backend uses the raw PTP
52/// storage ID; the WPD backend derives a stable token from the WPD storage object-ID.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
54pub struct StorageId(pub u64);
55
56impl StorageId {
57    /// Sentinel meaning "all storages".
58    pub const ALL: Self = StorageId(0xFFFF_FFFF);
59
60    /// Narrow to the PTP wire storage ID. See [`ObjectHandle::to_ptp`].
61    #[must_use]
62    pub(crate) fn to_ptp(self) -> PtpStorageId {
63        PtpStorageId(self.0 as u32)
64    }
65}
66
67impl From<PtpStorageId> for StorageId {
68    fn from(s: PtpStorageId) -> Self {
69        StorageId(u64::from(s.0))
70    }
71}
72
73/// Object format, as the MTP format code the device reports.
74///
75/// Kept as the raw 16-bit MTP format code (WPD exposes the same codes), with category helpers so
76/// callers don't switch on a backend-specific enum. Use [`ObjectFormat::is_association`] for the
77/// folder marker, or [`ObjectInfo::is_folder`] for the full folder test.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79pub struct ObjectFormat(pub u16);
80
81impl ObjectFormat {
82    /// Undefined / unknown format.
83    pub const UNDEFINED: Self = ObjectFormat(0x3000);
84    /// Association (a folder).
85    pub const ASSOCIATION: Self = ObjectFormat(0x3001);
86
87    /// The raw 16-bit MTP format code.
88    #[must_use]
89    pub fn code(self) -> u16 {
90        self.0
91    }
92
93    /// Whether this is the association (folder) format.
94    #[must_use]
95    pub fn is_association(self) -> bool {
96        self == Self::ASSOCIATION
97    }
98
99    /// Whether this is an audio format.
100    #[must_use]
101    pub fn is_audio(self) -> bool {
102        ObjectFormatCode::from(self.0).is_audio()
103    }
104
105    /// Whether this is a video format.
106    #[must_use]
107    pub fn is_video(self) -> bool {
108        ObjectFormatCode::from(self.0).is_video()
109    }
110
111    /// Whether this is an image format.
112    #[must_use]
113    pub fn is_image(self) -> bool {
114        ObjectFormatCode::from(self.0).is_image()
115    }
116}
117
118impl Default for ObjectFormat {
119    fn default() -> Self {
120        Self::UNDEFINED
121    }
122}
123
124impl From<ObjectFormatCode> for ObjectFormat {
125    fn from(c: ObjectFormatCode) -> Self {
126        ObjectFormat(c.into())
127    }
128}
129
130/// A calendar date and time as reported by a device (no timezone).
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct DateTime {
133    /// Year (e.g. 2026).
134    pub year: u16,
135    /// Month, 1-12.
136    pub month: u8,
137    /// Day, 1-31.
138    pub day: u8,
139    /// Hour, 0-23.
140    pub hour: u8,
141    /// Minute, 0-59.
142    pub minute: u8,
143    /// Second, 0-59.
144    pub second: u8,
145}
146
147impl From<PtpDateTime> for DateTime {
148    fn from(d: PtpDateTime) -> Self {
149        DateTime {
150            year: d.year,
151            month: d.month,
152            day: d.day,
153            hour: d.hour,
154            minute: d.minute,
155            second: d.second,
156        }
157    }
158}
159
160impl DateTime {
161    /// Convert to the PTP wire datetime (used when packing an upload's `ObjectInfo`).
162    #[must_use]
163    pub(crate) fn to_ptp(self) -> PtpDateTime {
164        PtpDateTime {
165            year: self.year,
166            month: self.month,
167            day: self.day,
168            hour: self.hour,
169            minute: self.minute,
170            second: self.second,
171        }
172    }
173}
174
175/// Type of storage medium.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
177pub enum StorageType {
178    /// Undefined storage type.
179    #[default]
180    Undefined,
181    /// Fixed read-only memory (e.g. internal flash exposed read-only).
182    FixedRom,
183    /// Removable read-only memory.
184    RemovableRom,
185    /// Fixed read-write memory (e.g. internal storage).
186    FixedRam,
187    /// Removable read-write memory (e.g. an SD card).
188    RemovableRam,
189    /// A code this library doesn't model.
190    Other,
191}
192
193impl From<PtpStorageType> for StorageType {
194    fn from(t: PtpStorageType) -> Self {
195        match t {
196            PtpStorageType::Undefined => StorageType::Undefined,
197            PtpStorageType::FixedRom => StorageType::FixedRom,
198            PtpStorageType::RemovableRom => StorageType::RemovableRom,
199            PtpStorageType::FixedRam => StorageType::FixedRam,
200            PtpStorageType::RemovableRam => StorageType::RemovableRam,
201            PtpStorageType::Unknown(_) => StorageType::Other,
202        }
203    }
204}
205
206/// Type of filesystem on a storage.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
208pub enum FilesystemType {
209    /// Undefined filesystem type.
210    #[default]
211    Undefined,
212    /// Flat (no folders).
213    Flat,
214    /// Hierarchical (folders).
215    Hierarchical,
216    /// DCF (camera file system).
217    Dcf,
218    /// A code this library doesn't model.
219    Other,
220}
221
222impl From<PtpFs> for FilesystemType {
223    fn from(t: PtpFs) -> Self {
224        match t {
225            PtpFs::Undefined => FilesystemType::Undefined,
226            PtpFs::GenericFlat => FilesystemType::Flat,
227            PtpFs::GenericHierarchical => FilesystemType::Hierarchical,
228            PtpFs::Dcf => FilesystemType::Dcf,
229            PtpFs::Unknown(_) => FilesystemType::Other,
230        }
231    }
232}
233
234/// Metadata for one object (file or folder) on a device.
235#[derive(Debug, Clone, Default)]
236pub struct ObjectInfo {
237    /// Opaque handle for this object (see [`ObjectHandle`]).
238    pub handle: ObjectHandle,
239    /// The storage this object lives on.
240    pub storage_id: StorageId,
241    /// Parent object, or [`ObjectHandle::ROOT`] for a top-level object.
242    pub parent: ObjectHandle,
243    /// File or folder name.
244    pub filename: String,
245    /// Size in bytes (0 for folders).
246    pub size: u64,
247    /// MTP format code.
248    pub format: ObjectFormat,
249    /// Creation time, if the device reported a valid one.
250    pub created: Option<DateTime>,
251    /// Modification time, if the device reported a valid one.
252    pub modified: Option<DateTime>,
253    /// Image width in pixels (0 if not an image or unknown).
254    pub image_width: u32,
255    /// Image height in pixels (0 if not an image or unknown).
256    pub image_height: u32,
257    /// Pre-resolved folder flag (see [`ObjectInfo::is_folder`]).
258    pub(crate) folder: bool,
259}
260
261impl ObjectInfo {
262    /// Whether this object is a folder.
263    #[must_use]
264    pub fn is_folder(&self) -> bool {
265        self.folder
266    }
267
268    /// Whether this object is a file (not a folder).
269    #[must_use]
270    pub fn is_file(&self) -> bool {
271        !self.folder
272    }
273
274    /// Build the neutral form from a PTP `ObjectInfo`, preserving the folder test (which on PTP
275    /// depends on both the format and the association type).
276    pub(crate) fn from_ptp(o: PtpObjectInfo) -> Self {
277        let folder = o.is_folder();
278        ObjectInfo {
279            handle: o.handle.into(),
280            storage_id: o.storage_id.into(),
281            parent: o.parent.into(),
282            filename: o.filename,
283            size: o.size,
284            format: o.format.into(),
285            created: o.created.map(Into::into),
286            modified: o.modified.map(Into::into),
287            image_width: o.image_width,
288            image_height: o.image_height,
289            folder,
290        }
291    }
292}
293
294/// Identity of a connected device, backend-neutral.
295///
296/// Protocol- and backend-specific detail (PTP operation lists, vendor extensions) lives on the
297/// low-level [`crate::ptp`] types, not here. What a device can *do* is reported separately via
298/// [`Capabilities`].
299#[derive(Debug, Clone, Default)]
300pub struct DeviceInfo {
301    /// Manufacturer name, if reported.
302    pub manufacturer: String,
303    /// Model name.
304    pub model: String,
305    /// Serial number, if reported.
306    pub serial_number: String,
307    /// Device firmware/software version, if reported.
308    pub device_version: String,
309}
310
311impl DeviceInfo {
312    pub(crate) fn from_ptp(d: &PtpDeviceInfo) -> Self {
313        DeviceInfo {
314            manufacturer: d.manufacturer.clone(),
315            model: d.model.clone(),
316            serial_number: d.serial_number.clone(),
317            device_version: d.device_version.clone(),
318        }
319    }
320}
321
322/// Description of a single storage on a device, backend-neutral.
323#[derive(Debug, Clone, Default)]
324pub struct StorageInfo {
325    /// Opaque identifier for this storage (see [`StorageId`]).
326    pub id: StorageId,
327    /// Human-readable description (e.g. "Internal shared storage").
328    pub description: String,
329    /// Volume identifier, if any.
330    pub volume_identifier: String,
331    /// Total capacity in bytes.
332    pub total_capacity: u64,
333    /// Free space in bytes.
334    pub free_space: u64,
335    /// Whether the storage accepts writes (uploads, deletes, renames).
336    pub is_writable: bool,
337    /// Storage medium type.
338    pub storage_type: StorageType,
339    /// Filesystem type.
340    pub filesystem_type: FilesystemType,
341}
342
343impl StorageInfo {
344    pub(crate) fn from_ptp(s: &PtpStorageInfo) -> Self {
345        StorageInfo {
346            // The PTP StorageInfo dataset doesn't carry its own id; the backend fills it in after
347            // the GetStorageInfo call that already knows the id.
348            id: StorageId::default(),
349            description: s.description.clone(),
350            volume_identifier: s.volume_identifier.clone(),
351            total_capacity: s.max_capacity,
352            free_space: s.free_space_bytes,
353            is_writable: s.access_capability == AccessCapability::ReadWrite,
354            storage_type: s.storage_type.into(),
355            filesystem_type: s.filesystem_type.into(),
356        }
357    }
358}
359
360/// What a connected device supports, derived per backend.
361///
362/// Replaces switching on backend-specific operation codes. The USB/PTP backend computes these from
363/// the device's advertised operations; the WPD backend computes them from WPD command support.
364/// Advertised support can still be wrong on some devices (see the Fujifilm quirk in `AGENTS.md`),
365/// so treat these as a strong hint, not a guarantee.
366#[derive(Debug, Clone, Copy, Default)]
367pub struct Capabilities {
368    /// Can create new files (upload).
369    pub can_upload: bool,
370    /// Can delete objects.
371    pub can_delete: bool,
372    /// Can rename objects.
373    pub can_rename: bool,
374    /// Can move objects between folders/storages.
375    pub can_move: bool,
376    /// Can copy objects.
377    pub can_copy: bool,
378    /// Can create folders.
379    pub can_create_folder: bool,
380    /// Can download a byte range / resume (not just whole files).
381    pub supports_partial_download: bool,
382    /// Can fetch thumbnails.
383    pub supports_thumbnails: bool,
384    /// Emits device events.
385    pub supports_events: bool,
386}
387
388impl Capabilities {
389    pub(crate) fn from_ptp_device_info(d: &PtpDeviceInfo) -> Self {
390        let op = |o: OperationCode| d.supports_operation(o);
391        Capabilities {
392            can_upload: op(OperationCode::SendObjectInfo) && op(OperationCode::SendObject),
393            can_delete: op(OperationCode::DeleteObject),
394            can_rename: d.supports_rename(),
395            can_move: op(OperationCode::MoveObject),
396            can_copy: op(OperationCode::CopyObject),
397            can_create_folder: op(OperationCode::SendObjectInfo),
398            supports_partial_download: op(OperationCode::GetPartialObject64)
399                || op(OperationCode::GetPartialObject),
400            supports_thumbnails: op(OperationCode::GetThumb),
401            supports_events: !d.events_supported.is_empty(),
402        }
403    }
404}