Skip to main content

mtp_rs/mtp/
object.rs

1//! Object-related types for MTP.
2
3use crate::mtp::{DateTime, ObjectFormat};
4use crate::ptp::{AssociationType, ObjectFormatCode, ObjectInfo as PtpObjectInfo};
5
6/// Information needed to create a new object.
7#[derive(Debug, Clone)]
8pub struct NewObjectInfo {
9    /// Filename (max 254 characters, no /, \, or null bytes)
10    pub filename: String,
11    /// File size in bytes (must match actual data sent)
12    pub size: u64,
13    /// Object format (auto-detected from extension if None)
14    pub format: Option<ObjectFormat>,
15    /// Modification time
16    pub modified: Option<DateTime>,
17}
18
19impl NewObjectInfo {
20    /// Create info for a file. Format auto-detected from extension.
21    #[must_use]
22    pub fn file(filename: impl Into<String>, size: u64) -> Self {
23        let filename = filename.into();
24        let format = detect_format_from_filename(&filename);
25        Self {
26            filename,
27            size,
28            format: Some(format),
29            modified: None,
30        }
31    }
32
33    /// Create info for a folder.
34    #[must_use]
35    pub fn folder(name: impl Into<String>) -> Self {
36        Self {
37            filename: name.into(),
38            size: 0,
39            format: Some(ObjectFormat::ASSOCIATION),
40            modified: None,
41        }
42    }
43
44    /// Create info with explicit format.
45    #[must_use]
46    pub fn with_format(filename: impl Into<String>, size: u64, format: ObjectFormat) -> Self {
47        Self {
48            filename: filename.into(),
49            size,
50            format: Some(format),
51            modified: None,
52        }
53    }
54
55    /// Set modification time.
56    #[must_use]
57    pub fn with_modified(mut self, modified: DateTime) -> Self {
58        self.modified = Some(modified);
59        self
60    }
61
62    /// Convert to PTP ObjectInfo for sending.
63    pub(crate) fn to_object_info(&self) -> PtpObjectInfo {
64        let format = self.format.unwrap_or(ObjectFormat::UNDEFINED);
65        let is_folder = format.is_association();
66
67        PtpObjectInfo {
68            format: ObjectFormatCode::from(format.code()),
69            size: self.size,
70            filename: self.filename.clone(),
71            modified: self.modified.map(DateTime::to_ptp),
72            association_type: if is_folder {
73                AssociationType::GenericFolder
74            } else {
75                AssociationType::None
76            },
77            ..Default::default()
78        }
79    }
80}
81
82/// Detect format from filename extension.
83fn detect_format_from_filename(filename: &str) -> ObjectFormat {
84    if let Some(ext) = filename.rsplit('.').next() {
85        ObjectFormat::from(ObjectFormatCode::from_extension(ext))
86    } else {
87        ObjectFormat::UNDEFINED
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    fn fmt(code: ObjectFormatCode) -> ObjectFormat {
96        ObjectFormat::from(code)
97    }
98
99    #[test]
100    fn test_new_object_info_file() {
101        let info = NewObjectInfo::file("test.mp3", 1000);
102        assert_eq!(info.filename, "test.mp3");
103        assert_eq!(info.size, 1000);
104        assert_eq!(info.format, Some(fmt(ObjectFormatCode::Mp3)));
105    }
106
107    #[test]
108    fn test_new_object_info_folder() {
109        let info = NewObjectInfo::folder("Music");
110        assert_eq!(info.filename, "Music");
111        assert_eq!(info.size, 0);
112        assert_eq!(info.format, Some(ObjectFormat::ASSOCIATION));
113    }
114
115    #[test]
116    fn test_format_detection() {
117        assert_eq!(
118            detect_format_from_filename("song.mp3"),
119            fmt(ObjectFormatCode::Mp3)
120        );
121        assert_eq!(
122            detect_format_from_filename("photo.jpg"),
123            fmt(ObjectFormatCode::Jpeg)
124        );
125        assert_eq!(
126            detect_format_from_filename("video.mp4"),
127            fmt(ObjectFormatCode::Mp4Container)
128        );
129        assert_eq!(
130            detect_format_from_filename("unknown.xyz"),
131            ObjectFormat::UNDEFINED
132        );
133    }
134
135    #[test]
136    fn test_with_format() {
137        let info =
138            NewObjectInfo::with_format("document.bin", 500, fmt(ObjectFormatCode::Executable));
139        assert_eq!(info.filename, "document.bin");
140        assert_eq!(info.size, 500);
141        assert_eq!(info.format, Some(fmt(ObjectFormatCode::Executable)));
142    }
143
144    #[test]
145    fn test_with_modified() {
146        let dt = DateTime {
147            year: 2024,
148            month: 6,
149            day: 15,
150            hour: 10,
151            minute: 30,
152            second: 0,
153        };
154        let info = NewObjectInfo::file("test.txt", 100).with_modified(dt);
155        assert_eq!(info.modified, Some(dt));
156    }
157
158    #[test]
159    fn test_to_object_info_file() {
160        let info = NewObjectInfo::file("test.mp3", 1000);
161        let ptp_info = info.to_object_info();
162
163        assert_eq!(ptp_info.format, ObjectFormatCode::Mp3);
164        assert_eq!(ptp_info.size, 1000);
165        assert_eq!(ptp_info.filename, "test.mp3");
166        assert_eq!(ptp_info.association_type, AssociationType::None);
167    }
168
169    #[test]
170    fn test_to_object_info_folder() {
171        let info = NewObjectInfo::folder("Music");
172        let ptp_info = info.to_object_info();
173
174        assert_eq!(ptp_info.format, ObjectFormatCode::Association);
175        assert_eq!(ptp_info.size, 0);
176        assert_eq!(ptp_info.filename, "Music");
177        assert_eq!(ptp_info.association_type, AssociationType::GenericFolder);
178    }
179
180    #[test]
181    fn test_format_detection_case_insensitive() {
182        assert_eq!(
183            detect_format_from_filename("SONG.MP3"),
184            fmt(ObjectFormatCode::Mp3)
185        );
186        assert_eq!(
187            detect_format_from_filename("Photo.JPG"),
188            fmt(ObjectFormatCode::Jpeg)
189        );
190    }
191
192    #[test]
193    fn test_format_detection_no_extension() {
194        assert_eq!(
195            detect_format_from_filename("noextension"),
196            ObjectFormat::UNDEFINED
197        );
198    }
199}