Skip to main content

mtp_rs/mtp/
object.rs

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