Skip to main content

mtp_rs/ptp/types/
objects.rs

1//! Object-related types for MTP/PTP.
2//!
3//! This module contains the [`ObjectInfo`] structure for file/folder metadata.
4
5use super::storage::{AssociationType, ProtectionStatus};
6use crate::ptp::pack::{
7    pack_datetime, pack_string, pack_u16, pack_u32, unpack_datetime, unpack_string, unpack_u16,
8    unpack_u32, DateTime,
9};
10use crate::ptp::{ObjectFormatCode, ObjectHandle, StorageId};
11
12// --- ObjectInfo Structure ---
13
14/// Object information returned by GetObjectInfo.
15///
16/// Contains file/folder metadata including name, size, timestamps, and hierarchy info.
17#[derive(Debug, Clone, Default)]
18pub struct ObjectInfo {
19    /// Object handle (set after parsing, not part of protocol data).
20    pub handle: ObjectHandle,
21    /// Storage containing this object.
22    pub storage_id: StorageId,
23    /// Object format code.
24    pub format: ObjectFormatCode,
25    /// Protection status.
26    pub protection_status: ProtectionStatus,
27    /// Object size in bytes.
28    ///
29    /// Note: Protocol uses u32, but we store as u64. Values of 0xFFFFFFFF indicate
30    /// the object is larger than 4GB (use GetObjectPropValue for actual size).
31    pub size: u64,
32    /// Thumbnail format.
33    pub thumb_format: ObjectFormatCode,
34    /// Thumbnail size in bytes.
35    pub thumb_size: u32,
36    /// Thumbnail width in pixels.
37    pub thumb_width: u32,
38    /// Thumbnail height in pixels.
39    pub thumb_height: u32,
40    /// Image width in pixels.
41    pub image_width: u32,
42    /// Image height in pixels.
43    pub image_height: u32,
44    /// Image bit depth.
45    pub image_bit_depth: u32,
46    /// Parent object handle (ROOT for root-level objects).
47    pub parent: ObjectHandle,
48    /// Association type (folder type).
49    pub association_type: AssociationType,
50    /// Association description.
51    pub association_desc: u32,
52    /// Sequence number.
53    pub sequence_number: u32,
54    /// Filename.
55    pub filename: String,
56    /// Creation timestamp.
57    pub created: Option<DateTime>,
58    /// Modification timestamp.
59    pub modified: Option<DateTime>,
60    /// Keywords string.
61    pub keywords: String,
62}
63
64impl ObjectInfo {
65    /// Parse ObjectInfo from a byte buffer.
66    ///
67    /// The buffer should contain the ObjectInfo dataset as returned by GetObjectInfo.
68    pub fn from_bytes(buf: &[u8]) -> Result<Self, crate::Error> {
69        let mut offset = 0;
70
71        // 1. StorageID (u32)
72        let storage_id = StorageId(unpack_u32(&buf[offset..])?);
73        offset += 4;
74
75        // 2. ObjectFormat (u16)
76        let format = ObjectFormatCode::from(unpack_u16(&buf[offset..])?);
77        offset += 2;
78
79        // 3. ProtectionStatus (u16)
80        let protection_status = ProtectionStatus::from(unpack_u16(&buf[offset..])?);
81        offset += 2;
82
83        // 4. ObjectCompressedSize (u32) - stored as u64, but protocol uses u32
84        let size = unpack_u32(&buf[offset..])? as u64;
85        offset += 4;
86
87        // 5. ThumbFormat (u16)
88        let thumb_format = ObjectFormatCode::from(unpack_u16(&buf[offset..])?);
89        offset += 2;
90
91        // 6. ThumbCompressedSize (u32)
92        let thumb_size = unpack_u32(&buf[offset..])?;
93        offset += 4;
94
95        // 7. ThumbPixWidth (u32)
96        let thumb_width = unpack_u32(&buf[offset..])?;
97        offset += 4;
98
99        // 8. ThumbPixHeight (u32)
100        let thumb_height = unpack_u32(&buf[offset..])?;
101        offset += 4;
102
103        // 9. ImagePixWidth (u32)
104        let image_width = unpack_u32(&buf[offset..])?;
105        offset += 4;
106
107        // 10. ImagePixHeight (u32)
108        let image_height = unpack_u32(&buf[offset..])?;
109        offset += 4;
110
111        // 11. ImageBitDepth (u32)
112        let image_bit_depth = unpack_u32(&buf[offset..])?;
113        offset += 4;
114
115        // 12. ParentObject (u32)
116        let parent = ObjectHandle(unpack_u32(&buf[offset..])?);
117        offset += 4;
118
119        // 13. AssociationType (u16)
120        let association_type = AssociationType::from(unpack_u16(&buf[offset..])?);
121        offset += 2;
122
123        // 14. AssociationDesc (u32)
124        let association_desc = unpack_u32(&buf[offset..])?;
125        offset += 4;
126
127        // 15. SequenceNumber (u32)
128        let sequence_number = unpack_u32(&buf[offset..])?;
129        offset += 4;
130
131        // 16. Filename (string)
132        let (filename, consumed) = unpack_string(&buf[offset..])?;
133        offset += consumed;
134
135        // 17. DateCreated (datetime string)
136        let (created, consumed) = unpack_datetime(&buf[offset..])?;
137        offset += consumed;
138
139        // 18. DateModified (datetime string)
140        let (modified, consumed) = unpack_datetime(&buf[offset..])?;
141        offset += consumed;
142
143        // 19. Keywords (string)
144        let (keywords, _consumed) = unpack_string(&buf[offset..])?;
145
146        Ok(ObjectInfo {
147            handle: ObjectHandle::default(), // Set by caller after parsing
148            storage_id,
149            format,
150            protection_status,
151            size,
152            thumb_format,
153            thumb_size,
154            thumb_width,
155            thumb_height,
156            image_width,
157            image_height,
158            image_bit_depth,
159            parent,
160            association_type,
161            association_desc,
162            sequence_number,
163            filename,
164            created,
165            modified,
166            keywords,
167        })
168    }
169
170    /// Serialize ObjectInfo to a byte buffer.
171    ///
172    /// Used for SendObjectInfo operation.
173    ///
174    /// Returns an error if the created or modified DateTime contains invalid values.
175    pub fn to_bytes(&self) -> Result<Vec<u8>, crate::Error> {
176        let mut buf = Vec::new();
177
178        // 1. StorageID (u32)
179        buf.extend_from_slice(&pack_u32(self.storage_id.0));
180
181        // 2. ObjectFormat (u16)
182        buf.extend_from_slice(&pack_u16(self.format.into()));
183
184        // 3. ProtectionStatus (u16)
185        buf.extend_from_slice(&pack_u16(self.protection_status.into()));
186
187        // 4. ObjectCompressedSize (u32) - cap at u32::MAX for >4GB files
188        let size_u32 = if self.size > u32::MAX as u64 {
189            u32::MAX
190        } else {
191            self.size as u32
192        };
193        buf.extend_from_slice(&pack_u32(size_u32));
194
195        // 5. ThumbFormat (u16)
196        buf.extend_from_slice(&pack_u16(self.thumb_format.into()));
197
198        // 6. ThumbCompressedSize (u32)
199        buf.extend_from_slice(&pack_u32(self.thumb_size));
200
201        // 7. ThumbPixWidth (u32)
202        buf.extend_from_slice(&pack_u32(self.thumb_width));
203
204        // 8. ThumbPixHeight (u32)
205        buf.extend_from_slice(&pack_u32(self.thumb_height));
206
207        // 9. ImagePixWidth (u32)
208        buf.extend_from_slice(&pack_u32(self.image_width));
209
210        // 10. ImagePixHeight (u32)
211        buf.extend_from_slice(&pack_u32(self.image_height));
212
213        // 11. ImageBitDepth (u32)
214        buf.extend_from_slice(&pack_u32(self.image_bit_depth));
215
216        // 12. ParentObject (u32)
217        buf.extend_from_slice(&pack_u32(self.parent.0));
218
219        // 13. AssociationType (u16)
220        buf.extend_from_slice(&pack_u16(self.association_type.into()));
221
222        // 14. AssociationDesc (u32)
223        buf.extend_from_slice(&pack_u32(self.association_desc));
224
225        // 15. SequenceNumber (u32)
226        buf.extend_from_slice(&pack_u32(self.sequence_number));
227
228        // 16. Filename (string)
229        buf.extend_from_slice(&pack_string(&self.filename));
230
231        // 17. DateCreated (datetime string)
232        if let Some(dt) = &self.created {
233            buf.extend_from_slice(&pack_datetime(dt)?);
234        } else {
235            buf.push(0x00); // Empty string
236        }
237
238        // 18. DateModified (datetime string)
239        if let Some(dt) = &self.modified {
240            buf.extend_from_slice(&pack_datetime(dt)?);
241        } else {
242            buf.push(0x00); // Empty string
243        }
244
245        // 19. Keywords (string)
246        buf.extend_from_slice(&pack_string(&self.keywords));
247
248        Ok(buf)
249    }
250
251    /// Check if this object is a folder.
252    ///
253    /// Returns true if the format is Association or the association type is GenericFolder.
254    #[must_use]
255    pub fn is_folder(&self) -> bool {
256        self.format == ObjectFormatCode::Association
257            || self.association_type == AssociationType::GenericFolder
258    }
259
260    /// Check if this object is a file.
261    ///
262    /// Returns true if this is not a folder.
263    #[must_use]
264    pub fn is_file(&self) -> bool {
265        !self.is_folder()
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::ptp::pack::{pack_datetime, pack_string, pack_u16, pack_u32, DateTime};
273
274    // --- ObjectInfo Tests ---
275
276    fn build_file_object_info_bytes() -> Vec<u8> {
277        let mut buf = Vec::new();
278
279        // StorageID: 0x00010001
280        buf.extend_from_slice(&pack_u32(0x00010001));
281        // ObjectFormat: JPEG (0x3801)
282        buf.extend_from_slice(&pack_u16(0x3801));
283        // ProtectionStatus: None (0)
284        buf.extend_from_slice(&pack_u16(0));
285        // ObjectCompressedSize: 1024 bytes
286        buf.extend_from_slice(&pack_u32(1024));
287        // ThumbFormat: JPEG (0x3801)
288        buf.extend_from_slice(&pack_u16(0x3801));
289        // ThumbCompressedSize: 512
290        buf.extend_from_slice(&pack_u32(512));
291        // ThumbPixWidth: 160
292        buf.extend_from_slice(&pack_u32(160));
293        // ThumbPixHeight: 120
294        buf.extend_from_slice(&pack_u32(120));
295        // ImagePixWidth: 1920
296        buf.extend_from_slice(&pack_u32(1920));
297        // ImagePixHeight: 1080
298        buf.extend_from_slice(&pack_u32(1080));
299        // ImageBitDepth: 24
300        buf.extend_from_slice(&pack_u32(24));
301        // ParentObject: 0x00000005
302        buf.extend_from_slice(&pack_u32(5));
303        // AssociationType: None (0)
304        buf.extend_from_slice(&pack_u16(0));
305        // AssociationDesc: 0
306        buf.extend_from_slice(&pack_u32(0));
307        // SequenceNumber: 1
308        buf.extend_from_slice(&pack_u32(1));
309        // Filename: "photo.jpg"
310        buf.extend_from_slice(&pack_string("photo.jpg"));
311        // DateCreated: "20240315T143022"
312        buf.extend_from_slice(
313            &pack_datetime(&DateTime {
314                year: 2024,
315                month: 3,
316                day: 15,
317                hour: 14,
318                minute: 30,
319                second: 22,
320            })
321            .unwrap(),
322        );
323        // DateModified: "20240316T090000"
324        buf.extend_from_slice(
325            &pack_datetime(&DateTime {
326                year: 2024,
327                month: 3,
328                day: 16,
329                hour: 9,
330                minute: 0,
331                second: 0,
332            })
333            .unwrap(),
334        );
335        // Keywords: ""
336        buf.push(0x00);
337
338        buf
339    }
340
341    #[test]
342    fn object_info_parse_file() {
343        let buf = build_file_object_info_bytes();
344        let info = ObjectInfo::from_bytes(&buf).unwrap();
345
346        assert_eq!(info.storage_id, StorageId(0x00010001));
347        assert_eq!(info.format, ObjectFormatCode::Jpeg);
348        assert_eq!(info.protection_status, ProtectionStatus::None);
349        assert_eq!(info.size, 1024);
350        assert_eq!(info.thumb_format, ObjectFormatCode::Jpeg);
351        assert_eq!(info.thumb_size, 512);
352        assert_eq!(info.thumb_width, 160);
353        assert_eq!(info.thumb_height, 120);
354        assert_eq!(info.image_width, 1920);
355        assert_eq!(info.image_height, 1080);
356        assert_eq!(info.image_bit_depth, 24);
357        assert_eq!(info.parent, ObjectHandle(5));
358        assert_eq!(info.association_type, AssociationType::None);
359        assert_eq!(info.association_desc, 0);
360        assert_eq!(info.sequence_number, 1);
361        assert_eq!(info.filename, "photo.jpg");
362        assert!(info.created.is_some());
363        let created = info.created.unwrap();
364        assert_eq!(created.year, 2024);
365        assert_eq!(created.month, 3);
366        assert_eq!(created.day, 15);
367        assert!(info.modified.is_some());
368        assert_eq!(info.keywords, "");
369
370        assert!(info.is_file());
371        assert!(!info.is_folder());
372    }
373
374    fn build_folder_object_info_bytes() -> Vec<u8> {
375        let mut buf = Vec::new();
376
377        // StorageID: 0x00010001
378        buf.extend_from_slice(&pack_u32(0x00010001));
379        // ObjectFormat: Association (0x3001)
380        buf.extend_from_slice(&pack_u16(0x3001));
381        // ProtectionStatus: None (0)
382        buf.extend_from_slice(&pack_u16(0));
383        // ObjectCompressedSize: 0
384        buf.extend_from_slice(&pack_u32(0));
385        // ThumbFormat: Undefined (0x3000)
386        buf.extend_from_slice(&pack_u16(0x3000));
387        // ThumbCompressedSize: 0
388        buf.extend_from_slice(&pack_u32(0));
389        // ThumbPixWidth: 0
390        buf.extend_from_slice(&pack_u32(0));
391        // ThumbPixHeight: 0
392        buf.extend_from_slice(&pack_u32(0));
393        // ImagePixWidth: 0
394        buf.extend_from_slice(&pack_u32(0));
395        // ImagePixHeight: 0
396        buf.extend_from_slice(&pack_u32(0));
397        // ImageBitDepth: 0
398        buf.extend_from_slice(&pack_u32(0));
399        // ParentObject: ROOT (0)
400        buf.extend_from_slice(&pack_u32(0));
401        // AssociationType: GenericFolder (1)
402        buf.extend_from_slice(&pack_u16(1));
403        // AssociationDesc: 0
404        buf.extend_from_slice(&pack_u32(0));
405        // SequenceNumber: 0
406        buf.extend_from_slice(&pack_u32(0));
407        // Filename: "DCIM"
408        buf.extend_from_slice(&pack_string("DCIM"));
409        // DateCreated: empty
410        buf.push(0x00);
411        // DateModified: empty
412        buf.push(0x00);
413        // Keywords: ""
414        buf.push(0x00);
415
416        buf
417    }
418
419    #[test]
420    fn object_info_parse_folder() {
421        let buf = build_folder_object_info_bytes();
422        let info = ObjectInfo::from_bytes(&buf).unwrap();
423
424        assert_eq!(info.format, ObjectFormatCode::Association);
425        assert_eq!(info.association_type, AssociationType::GenericFolder);
426        assert_eq!(info.filename, "DCIM");
427        assert_eq!(info.parent, ObjectHandle::ROOT);
428        assert!(info.created.is_none());
429        assert!(info.modified.is_none());
430
431        assert!(info.is_folder());
432        assert!(!info.is_file());
433    }
434
435    #[test]
436    fn object_info_to_bytes_roundtrip() {
437        let original = ObjectInfo {
438            handle: ObjectHandle(42),
439            storage_id: StorageId(0x00010001),
440            format: ObjectFormatCode::Jpeg,
441            protection_status: ProtectionStatus::None,
442            size: 2048,
443            thumb_format: ObjectFormatCode::Jpeg,
444            thumb_size: 256,
445            thumb_width: 80,
446            thumb_height: 60,
447            image_width: 800,
448            image_height: 600,
449            image_bit_depth: 24,
450            parent: ObjectHandle(10),
451            association_type: AssociationType::None,
452            association_desc: 0,
453            sequence_number: 5,
454            filename: "test.jpg".to_string(),
455            created: Some(DateTime {
456                year: 2024,
457                month: 6,
458                day: 15,
459                hour: 10,
460                minute: 30,
461                second: 0,
462            }),
463            modified: Some(DateTime {
464                year: 2024,
465                month: 6,
466                day: 16,
467                hour: 11,
468                minute: 45,
469                second: 30,
470            }),
471            keywords: "test,photo".to_string(),
472        };
473
474        let bytes = original.to_bytes().unwrap();
475        let parsed = ObjectInfo::from_bytes(&bytes).unwrap();
476
477        assert_eq!(parsed.storage_id, original.storage_id);
478        assert_eq!(parsed.format, original.format);
479        assert_eq!(parsed.protection_status, original.protection_status);
480        assert_eq!(parsed.size, original.size);
481        assert_eq!(parsed.thumb_format, original.thumb_format);
482        assert_eq!(parsed.thumb_size, original.thumb_size);
483        assert_eq!(parsed.thumb_width, original.thumb_width);
484        assert_eq!(parsed.thumb_height, original.thumb_height);
485        assert_eq!(parsed.image_width, original.image_width);
486        assert_eq!(parsed.image_height, original.image_height);
487        assert_eq!(parsed.image_bit_depth, original.image_bit_depth);
488        assert_eq!(parsed.parent, original.parent);
489        assert_eq!(parsed.association_type, original.association_type);
490        assert_eq!(parsed.association_desc, original.association_desc);
491        assert_eq!(parsed.sequence_number, original.sequence_number);
492        assert_eq!(parsed.filename, original.filename);
493        assert_eq!(parsed.created, original.created);
494        assert_eq!(parsed.modified, original.modified);
495        assert_eq!(parsed.keywords, original.keywords);
496    }
497
498    #[test]
499    fn object_info_to_bytes_large_size() {
500        let info = ObjectInfo {
501            size: 5_000_000_000, // 5GB, larger than u32::MAX
502            ..Default::default()
503        };
504
505        let bytes = info.to_bytes().unwrap();
506        let parsed = ObjectInfo::from_bytes(&bytes).unwrap();
507
508        // Should be capped at u32::MAX when serializing
509        assert_eq!(parsed.size, u32::MAX as u64);
510    }
511
512    #[test]
513    fn object_info_is_folder_by_format() {
514        let info = ObjectInfo {
515            format: ObjectFormatCode::Association,
516            association_type: AssociationType::None,
517            ..Default::default()
518        };
519        assert!(info.is_folder());
520    }
521
522    #[test]
523    fn object_info_is_folder_by_association() {
524        let info = ObjectInfo {
525            format: ObjectFormatCode::Undefined,
526            association_type: AssociationType::GenericFolder,
527            ..Default::default()
528        };
529        assert!(info.is_folder());
530    }
531
532    #[test]
533    fn object_info_is_file() {
534        let info = ObjectInfo {
535            format: ObjectFormatCode::Jpeg,
536            association_type: AssociationType::None,
537            ..Default::default()
538        };
539        assert!(info.is_file());
540        assert!(!info.is_folder());
541    }
542
543    #[test]
544    fn object_info_parse_insufficient_bytes() {
545        let buf = vec![0x00; 10]; // Not enough bytes
546        assert!(ObjectInfo::from_bytes(&buf).is_err());
547    }
548
549    #[test]
550    fn object_info_default() {
551        let info = ObjectInfo::default();
552        assert_eq!(info.storage_id, StorageId::default());
553        assert_eq!(info.format, ObjectFormatCode::Undefined);
554        assert_eq!(info.protection_status, ProtectionStatus::None);
555        assert_eq!(info.size, 0);
556        assert_eq!(info.filename, "");
557        assert!(info.created.is_none());
558        assert!(info.modified.is_none());
559    }
560
561    // Fuzz tests using shared macros
562    crate::fuzz_bytes!(fuzz_object_info, ObjectInfo, 200);
563
564    #[test]
565    fn object_info_minimum_valid() {
566        // ObjectInfo has many fixed fields before strings
567        // StorageID(4) + Format(2) + Protection(2) + Size(4) + ThumbFormat(2) + ThumbSize(4) +
568        // ThumbW(4) + ThumbH(4) + ImgW(4) + ImgH(4) + BitDepth(4) + Parent(4) + AssocType(2) +
569        // AssocDesc(4) + SeqNum(4) = 52 bytes + 4 strings
570        assert!(ObjectInfo::from_bytes(&[]).is_err());
571        assert!(ObjectInfo::from_bytes(&[0; 51]).is_err());
572        assert!(ObjectInfo::from_bytes(&[0; 52]).is_err()); // Still need string data
573    }
574
575    #[test]
576    fn object_info_size_u32_max() {
577        // When size is u32::MAX (0xFFFFFFFF), it indicates >4GB file
578        let mut buf = build_file_object_info_bytes();
579        // Replace size field (bytes 8-11, after StorageID(4) + Format(2) + Protection(2))
580        buf[8] = 0xFF;
581        buf[9] = 0xFF;
582        buf[10] = 0xFF;
583        buf[11] = 0xFF;
584
585        let info = ObjectInfo::from_bytes(&buf).unwrap();
586        assert_eq!(info.size, u32::MAX as u64);
587    }
588}