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}