Skip to main content

apple_bundle/
lib.rs

1#![allow(clippy::doc_lazy_continuation)]
2
3//! # Apple Bundle Resources
4//!
5//! Resources located in an app, framework, or plugin bundle.
6//!
7//! A bundle is a directory with a standardized hierarchical structure that holds
8//! executable code and the resources used by that code. The bundle contains resources
9//! that may be accessed at runtime, such as images, audio files, user interface files,
10//! and property lists.
11//!
12//! Official documentation: https://developer.apple.com/documentation/bundleresources
13
14/// Entitlements
15pub mod entitlements;
16/// Information Property List
17pub mod info_plist;
18/// Prelude
19#[allow(ambiguous_glob_reexports)]
20pub mod prelude {
21    pub use super::entitlements::prelude::*;
22    pub use super::info_plist::prelude::*;
23    #[cfg(feature = "plist")]
24    pub use plist;
25}
26#[cfg(feature = "plist")]
27pub use plist::{
28    self, from_bytes, from_file, from_reader, from_reader_xml, to_file_binary, to_file_xml,
29    to_writer_binary, to_writer_xml,
30};
31
32use serde::{ser::SerializeSeq, Serialize, Serializer};
33
34fn serialize_enum_option<S: Serializer, T: Serialize>(
35    value: &Option<T>,
36    s: S,
37) -> Result<S::Ok, S::Error> {
38    s.serialize_str(&serde_plain::to_string(value).unwrap())
39}
40
41fn serialize_vec_enum_option<S: Serializer, T: Serialize>(
42    value: &Option<Vec<T>>,
43    s: S,
44) -> Result<S::Ok, S::Error> {
45    match value {
46        Some(ref val) => {
47            let mut seq = s.serialize_seq(Some(val.len()))?;
48            for element in val.iter() {
49                seq.serialize_element(&serde_plain::to_string(element).unwrap())?;
50            }
51            seq.end()
52        }
53        None => panic!("unsupported"),
54    }
55}
56
57fn serialize_option<S, T>(value: &Option<T>, ser: S) -> Result<S::Ok, S::Error>
58where
59    S: Serializer,
60    T: Serialize,
61{
62    value
63        .as_ref()
64        .expect(r#"`serialize_option` must be used with `skip_serializing_if = "Option::is_none"`"#)
65        .serialize(ser)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::prelude::*;
71
72    pub const PLIST_FILE_NAME: &str = "Info.plist";
73
74    pub const PLIST_TEST_EXAMPLE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
75<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
76<plist version="1.0">
77<dict>
78    <key>CFBundlePackageType</key>
79    <string>APPL</string>
80    <key>LSApplicationCategoryType</key>
81    <string>public.app-category.business</string>
82    <key>CFBundleIdentifier</key>
83    <string>com.test.test-id</string>
84    <key>CFBundleName</key>
85    <string>Test</string>
86    <key>CFBundleVersion</key>
87    <string>1</string>
88    <key>CFBundleShortVersionString</key>
89    <string>1.0</string>
90    <key>CFBundleInfoDictionaryVersion</key>
91    <string>1.0</string>
92    <key>CFBundleDevelopmentRegion</key>
93    <string>en</string>
94    <key>UILaunchStoryboardName</key>
95    <string>LaunchScreen</string>
96    <key>UISupportedInterfaceOrientations</key>
97    <array>
98        <string>UIInterfaceOrientationPortrait</string>
99        <string>UIInterfaceOrientationPortraitUpsideDown</string>
100        <string>UIInterfaceOrientationLandscapeLeft</string>
101        <string>UIInterfaceOrientationLandscapeRight</string>
102    </array>
103    <key>UIRequiresFullScreen</key>
104    <false/>
105    <key>CFBundleExecutable</key>
106    <string>test</string>
107</dict>
108</plist>"#;
109
110    #[test]
111    fn test_plist_equality() {
112        let dir = tempfile::tempdir().unwrap();
113        let properties = InfoPlist {
114            localization: Localization {
115                bundle_development_region: Some("en".to_owned()),
116                ..Default::default()
117            },
118            launch: Launch {
119                bundle_executable: Some("test".to_owned()),
120                ..Default::default()
121            },
122            identification: Identification {
123                bundle_identifier: "com.test.test-id".to_owned(),
124                ..Default::default()
125            },
126            bundle_version: BundleVersion {
127                bundle_version: Some("1".to_owned()),
128                bundle_info_dictionary_version: Some("1.0".to_owned()),
129                bundle_short_version_string: Some("1.0".to_owned()),
130                ..Default::default()
131            },
132            naming: Naming {
133                bundle_name: Some("Test".to_owned()),
134                ..Default::default()
135            },
136            categorization: Categorization {
137                bundle_package_type: Some("APPL".to_owned()),
138                application_category_type: Some(AppCategoryType::Business),
139            },
140            launch_interface: LaunchInterface {
141                launch_storyboard_name: Some("LaunchScreen".to_owned()),
142                ..Default::default()
143            },
144            styling: Styling {
145                requires_full_screen: Some(false),
146                ..Default::default()
147            },
148            orientation: Orientation {
149                supported_interface_orientations: Some(vec![
150                    InterfaceOrientation::Portrait,
151                    InterfaceOrientation::PortraitUpsideDown,
152                    InterfaceOrientation::LandscapeLeft,
153                    InterfaceOrientation::LandscapeRight,
154                ]),
155                ..Default::default()
156            },
157            ..Default::default()
158        };
159        // Create Info.plist file
160        let file_path = dir.path().join(PLIST_FILE_NAME);
161        let file = std::fs::File::create(file_path).unwrap();
162        // Write to Info.plist file
163        plist::to_writer_xml(file, &properties).unwrap();
164        // Read Info.plist
165        let file_path = dir.path().join(PLIST_FILE_NAME);
166        let result = std::fs::read_to_string(&file_path).unwrap();
167        assert_eq!(result, PLIST_TEST_EXAMPLE.replace("    ", "\t"));
168        // Parse Info.plist
169        let got_props: InfoPlist = plist::from_bytes(result.as_bytes()).unwrap();
170        assert_eq!(properties, got_props);
171    }
172
173    #[test]
174    fn default_dictionary_reexports_share_one_type() {
175        let dictionary = crate::info_plist::app_execution::DefaultDictionary {
176            default: "value".to_owned(),
177        };
178        let _: crate::info_plist::data_and_storage::DefaultDictionary = dictionary.clone();
179        let _: crate::info_plist::protected_resources::DefaultDictionary = dictionary;
180    }
181
182    #[test]
183    fn opengl_es_3_capability_uses_its_own_plist_value() {
184        assert_eq!(
185            serde_plain::to_string(&DeviceCapabilities::Opengles3).unwrap(),
186            "opengles-3"
187        );
188    }
189}