Skip to main content

ithmb_core/
device_profiles.rs

1//! Device-specific format-ID lookup tables — maps each known iPod/iPhone
2//! generation to the format IDs it generates for its thumbnail caches
3//! (`PhotoDB` & `ArtworkDB`).
4//!
5//! Synthesised from iOpenPod, `OrgZ`, libgpod, gnupod, and the 22-repo
6//! research sweep. **Read-only reference** — not used during decode.
7
8/// A single format entry known to a device generation.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct DeviceFormatInfo {
11    /// Numeric format ID (the big-endian 4-byte prefix in .ithmb files).
12    pub format_id: i32,
13    /// Human-readable description (dimensions and encoding).
14    pub description: &'static str,
15}
16
17/// A device generation and the set of format IDs it produces.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct DeviceProfile {
20    /// Human-readable device name (e.g. "iPod Classic 5G (Video)").
21    pub name: &'static str,
22    /// Slice of format entries known to this device.
23    pub formats: &'static [DeviceFormatInfo],
24}
25
26// ---------------------------------------------------------------------------
27// Shared format tables — identical-format devices share a single array
28// ---------------------------------------------------------------------------
29
30macro_rules! formats {
31    ($($id:expr => $desc:expr),+ $(,)?) => {
32        &[$(DeviceFormatInfo { format_id: $id, description: $desc }),+]
33    };
34}
35
36static CLASSIC_5G: &[DeviceFormatInfo] = formats![
37    1019 => "720×480 YUV422 interlaced full-screen",
38    1024 => "320×240 RGB565 photo",
39    1027 => "100×100 RGB565 cover art",
40    1028 => "100×100 RGB565",
41    1029 => "200×200 RGB565",
42    1031 => "42×42 RGB565",
43    1032 => "42×37 RGB565",
44];
45
46static CLASSIC_5_5G: &[DeviceFormatInfo] = formats![
47    1019 => "720×480 YUV422 interlaced full-screen",
48    1024 => "320×240 RGB565 photo",
49    1027 => "100×100 RGB565 cover art",
50    1028 => "100×100 RGB565",
51    1029 => "200×200 RGB565",
52    1031 => "42×42 RGB565",
53    1032 => "42×37 RGB565",
54    1055 => "128×128 RGB565 cover art medium",
55    1056 => "80×80 RGB565",
56];
57
58static CLASSIC_6G: &[DeviceFormatInfo] = formats![
59    1024 => "320×240 RGB565 photo",
60    1055 => "128×128 RGB565 cover art",
61    1060 => "320×320 RGB565 cover art large",
62    1061 => "56×56 RGB565 cover art small",
63    1066 => "64×64 RGB565 photo",
64    1067 => "720×480 YCbCr420 padded full-screen",
65    1068 => "128×128 RGB565 cover art",
66];
67
68static NANO_1G: &[DeviceFormatInfo] = formats![
69    1024 => "320×240 RGB565",
70    1027 => "100×100 RGB565",
71];
72
73static NANO_2G: &[DeviceFormatInfo] = formats![
74    1019 => "720×480 YUV422",
75    1027 => "100×100",
76    1028 => "100×100",
77    1029 => "200×200",
78    1032 => "42×37",
79];
80
81static NANO_3G: &[DeviceFormatInfo] = formats![
82    1066 => "64×64 RGB565",
83    1067 => "720×480 YCbCr420 padded",
84    1068 => "128×128",
85    1071 => "240×240",
86    1073 => "240×240",
87    1074 => "50×50",
88];
89
90static NANO_4G: &[DeviceFormatInfo] = formats![
91    1071 => "240×240",
92    1073 => "240×240",
93    1074 => "50×50",
94    1078 => "80×80",
95    1079 => "80×80",
96    1083 => "240×320",
97    1084 => "240×240",
98    1085 => "88×88",
99    1087 => "384×384",
100    1089 => "58×58",
101    1092 => "80×80",
102    1093 => "512×512",
103];
104
105static NANO_5G: &[DeviceFormatInfo] = formats![
106    1087 => "384×384 RGB565",
107    1092 => "80×80",
108    1093 => "512×512",
109];
110
111static NANO_6G: &[DeviceFormatInfo] = formats![
112    1084 => "240×240",
113    1092 => "80×80",
114    1093 => "512×512",
115];
116
117static NANO_7G: &[DeviceFormatInfo] = formats![
118    1007 => "480×864 RGB565 full-res",
119    1010 => "240×240 RGB565 cover art",
120];
121
122// iPod Video 5G shares the same table as Classic 5G
123static MINI_1G_2G: &[DeviceFormatInfo] = formats![
124    1024 => "320×240 RGB565",
125    1027 => "100×100",
126];
127
128static PHOTO_4G: &[DeviceFormatInfo] = formats![
129    1013 => "220×176 RGB565 big-endian",
130    1015 => "130×88 RGB565",
131    1016 => "140×140",
132    1019 => "720×480 YUV422",
133];
134
135// iPod Touch 1G/2G and Touch 3G/4G share the same table
136static TOUCH: &[DeviceFormatInfo] = formats![
137    3001 => "256×256 RGB555",
138    3002 => "128×128 RGB555",
139    3003 => "64×64 RGB555",
140    3004 => "56×55 RGB555",
141    3005 => "320×320 RGB555",
142    3008 => "640×480 RGB555",
143    3009 => "160×120 RGB555",
144    3011 => "80×79 RGB555",
145];
146
147// iPhone 1G/2G and iPhone 3G/3GS share the same table
148static IPHONE: &[DeviceFormatInfo] = formats![
149    3001 => "256×256 RGB555",
150    3002 => "128×128 RGB555",
151    3003 => "64×64 RGB555",
152    3004 => "56×55 RGB555",
153    3005 => "320×320 RGB555",
154    3008 => "640×480 RGB555",
155    3009 => "160×120 RGB555",
156    3011 => "80×79 RGB555",
157];
158
159static ROKR_E1: &[DeviceFormatInfo] = formats![
160    2002 => "50×50 RGB565 big-endian",
161    2003 => "150×150 RGB565 big-endian",
162];
163
164// ---------------------------------------------------------------------------
165// Master device list (18 profiles, some sharing format tables above)
166// ---------------------------------------------------------------------------
167
168/// All known device profiles.
169pub static DEVICE_PROFILES: &[DeviceProfile] = &[
170    DeviceProfile {
171        name: "iPod Classic 5G (Video)",
172        formats: CLASSIC_5G,
173    },
174    DeviceProfile {
175        name: "iPod Classic 5.5G (Enhanced)",
176        formats: CLASSIC_5_5G,
177    },
178    DeviceProfile {
179        name: "iPod Classic 6G (Thin)",
180        formats: CLASSIC_6G,
181    },
182    DeviceProfile {
183        name: "iPod Nano 1G",
184        formats: NANO_1G,
185    },
186    DeviceProfile {
187        name: "iPod Nano 2G",
188        formats: NANO_2G,
189    },
190    DeviceProfile {
191        name: "iPod Nano 3G",
192        formats: NANO_3G,
193    },
194    DeviceProfile {
195        name: "iPod Nano 4G",
196        formats: NANO_4G,
197    },
198    DeviceProfile {
199        name: "iPod Nano 5G",
200        formats: NANO_5G,
201    },
202    DeviceProfile {
203        name: "iPod Nano 6G",
204        formats: NANO_6G,
205    },
206    DeviceProfile {
207        name: "iPod Nano 7G",
208        formats: NANO_7G,
209    },
210    DeviceProfile {
211        name: "iPod Video 5G",
212        formats: CLASSIC_5G,
213    },
214    DeviceProfile {
215        name: "iPod Mini 1G/2G",
216        formats: MINI_1G_2G,
217    },
218    DeviceProfile {
219        name: "iPod Photo 4G",
220        formats: PHOTO_4G,
221    },
222    DeviceProfile {
223        name: "iPod Touch 1G/2G",
224        formats: TOUCH,
225    },
226    DeviceProfile {
227        name: "iPod Touch 3G/4G",
228        formats: TOUCH,
229    },
230    DeviceProfile {
231        name: "iPhone 1G/2G",
232        formats: IPHONE,
233    },
234    DeviceProfile {
235        name: "iPhone 3G/3GS",
236        formats: IPHONE,
237    },
238    DeviceProfile {
239        name: "Motorola ROKR E1",
240        formats: ROKR_E1,
241    },
242];
243
244// ---------------------------------------------------------------------------
245// Public lookup functions
246// ---------------------------------------------------------------------------
247
248/// Find a device profile by name (case-insensitive substring match).
249#[must_use]
250pub fn find_device(name: &str) -> Option<&'static DeviceProfile> {
251    let lower = name.to_ascii_lowercase();
252    DEVICE_PROFILES
253        .iter()
254        .find(|p| p.name.to_ascii_lowercase().contains(&lower))
255}
256
257/// Search **all** device profiles for every format entry matching `format_id`.
258#[must_use]
259pub fn find_formats_by_id(format_id: i32) -> Vec<&'static DeviceFormatInfo> {
260    let mut results: Vec<&'static DeviceFormatInfo> = Vec::new();
261    for profile in DEVICE_PROFILES {
262        if let Some(info) = profile.formats.iter().find(|f| f.format_id == format_id) {
263            results.push(info);
264        }
265    }
266    results
267}
268
269/// Return a reference to the complete device-profiles table.
270#[must_use]
271pub fn all_device_profiles() -> &'static [DeviceProfile] {
272    DEVICE_PROFILES
273}
274
275// ---------------------------------------------------------------------------
276// Tests
277// ---------------------------------------------------------------------------
278
279#[cfg(test)]
280#[allow(clippy::unwrap_used)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn has_18_device_profiles() {
286        assert_eq!(DEVICE_PROFILES.len(), 18);
287    }
288
289    #[test]
290    fn find_classic_5g_by_name() {
291        let device = find_device("iPod Classic 5G").expect("should find Classic 5G");
292        assert_eq!(device.name, "iPod Classic 5G (Video)");
293        assert_eq!(device.formats.len(), 7);
294    }
295
296    #[test]
297    fn find_case_insensitive() {
298        let device = find_device("IPOD CLASSIC 5G").expect("case-insensitive");
299        assert_eq!(device.name, "iPod Classic 5G (Video)");
300    }
301
302    #[test]
303    fn nano_7g_has_two_formats() {
304        let device = find_device("iPod Nano 7G").expect("Nano 7G");
305        assert_eq!(device.formats.len(), 2);
306        assert!(device.formats.iter().any(|f| f.format_id == 1007));
307    }
308
309    #[test]
310    fn touch_3g_reuses_touch_array() {
311        let t3 = find_device("iPod Touch 3G").expect("Touch 3G/4G");
312        let t1 = find_device("iPod Touch 1G").expect("Touch 1G/2G");
313        assert_eq!(t3.formats.len(), 8);
314        assert!(
315            std::ptr::eq(t3.formats, t1.formats),
316            "Touch devices must share format table"
317        );
318    }
319
320    #[test]
321    fn iphone_3g_reuses_iphone_array() {
322        let i1 = find_device("iPhone 1G").expect("iPhone 1G/2G");
323        let i3 = find_device("iPhone 3G").expect("iPhone 3G/3GS");
324        assert!(
325            std::ptr::eq(i1.formats, i3.formats),
326            "iPhone devices must share format table"
327        );
328    }
329
330    #[test]
331    fn video_5g_reuses_classic_5g_array() {
332        let c5 = find_device("iPod Classic 5G").expect("Classic 5G");
333        let v5 = find_device("iPod Video 5G").expect("Video 5G");
334        assert!(
335            std::ptr::eq(c5.formats, v5.formats),
336            "Video 5G must share Classic 5G table"
337        );
338    }
339
340    #[test]
341    fn rokr_e1_has_two_formats() {
342        let device = find_device("Motorola ROKR E1").expect("ROKR E1");
343        assert_eq!(device.formats.len(), 2);
344        assert!(device.formats.iter().any(|f| f.format_id == 2003));
345    }
346
347    #[test]
348    fn nonexistent_device_returns_none() {
349        assert!(find_device("iPod Shuffle").is_none());
350    }
351
352    #[test]
353    fn find_1019_across_devices() {
354        let results = find_formats_by_id(1019);
355        assert!(!results.is_empty());
356        assert!(results.iter().all(|f| f.format_id == 1019));
357    }
358
359    #[test]
360    fn find_3001_in_four_devices() {
361        let results = find_formats_by_id(3001);
362        assert_eq!(results.len(), 4);
363    }
364
365    #[test]
366    fn nonexistent_format_returns_empty() {
367        assert!(find_formats_by_id(9999).is_empty());
368    }
369
370    #[test]
371    fn all_device_profiles_is_stable() {
372        assert!(std::ptr::eq(all_device_profiles(), DEVICE_PROFILES));
373    }
374
375    #[test]
376    fn nano_4g_has_most_formats() {
377        let device = find_device("iPod Nano 4G").expect("Nano 4G");
378        assert_eq!(device.formats.len(), 12);
379    }
380
381    #[test]
382    fn every_device_has_formats() {
383        for p in DEVICE_PROFILES {
384            assert!(!p.formats.is_empty(), "{}", p.name);
385        }
386    }
387
388    #[test]
389    fn every_format_has_description() {
390        for p in DEVICE_PROFILES {
391            for f in p.formats {
392                assert!(!f.description.is_empty());
393            }
394        }
395    }
396}