openpack 0.2.2

Safe archive-reader for ZIP-derived formats (ZIP, CRX, JAR, APK, IPA) with BOM-safe checks.
Documentation
#![allow(unused_imports)]
#[cfg(feature = "apk")]
use crate::manifest::parse_android_manifest;
#[cfg(feature = "ipa")]
use crate::manifest::parse_info_plist;
use crate::types::{OpenPack, OpenPackError};

impl OpenPack {
    #[cfg(feature = "apk")]
    pub fn read_android_manifest(&self) -> Result<crate::AndroidManifest, OpenPackError> {
        let bytes = self.read_entry("AndroidManifest.xml")?;
        match parse_android_manifest(&bytes) {
            Some(manifest) => Ok(manifest),
            None => {
                // If the manifest is not valid UTF-8 it is likely AXML (binary Android XML).
                if std::str::from_utf8(&bytes).is_err() {
                    Err(OpenPackError::InvalidArchive(
                        "failed parsing AndroidManifest.xml: binary AXML not supported. Fix: enable an AXML parser or add support via a feature like 'apk-axml'.".into(),
                    ))
                } else {
                    Err(OpenPackError::InvalidArchive(
                        "failed parsing AndroidManifest.xml".into(),
                    ))
                }
            }
        }
    }

    #[cfg(feature = "ipa")]
    pub fn read_info_plist(&self) -> Result<crate::IpaInfoPlist, OpenPackError> {
        let entry_name = self
            .entries()?
            .into_iter()
            .find_map(|entry| {
                entry
                    .name
                    .strip_prefix("Payload/")
                    .filter(|inner| inner.ends_with(".app/Info.plist"))
                    .map(|_| entry.name.clone())
            })
            .ok_or_else(|| OpenPackError::MissingEntry("Info.plist".into()))?;

        let bytes = self.read_entry(&entry_name)?;
        // Binary property lists start with the magic "bplist00".
        if bytes.starts_with(b"bplist00") {
            return Err(OpenPackError::InvalidArchive(
                "Info.plist appears to be a binary plist (bplist00); binary plists are not supported. Fix: enable plist parsing (e.g., add the 'plist' crate behind an 'ipa-plist' feature).".into(),
            ));
        }

        let text = String::from_utf8_lossy(&bytes);
        parse_info_plist(&text)
            .ok_or_else(|| OpenPackError::InvalidArchive("failed parsing Info.plist".into()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::archive::test_helpers::*;
    use zip::CompressionMethod;

    #[cfg(feature = "apk")]
    #[test]
    fn parse_android_manifest() {
        let archive = Scratch::new("app.apk");
        let manifest = r#"<manifest package="com.example.app" versionName="1.2.3" versionCode="5"><uses-sdk android:minSdkVersion="21"/></manifest>"#;
        write_zip(
            &archive.path,
            &[(
                "AndroidManifest.xml",
                manifest.as_bytes(),
                CompressionMethod::Stored,
            )],
        );
        let pack = OpenPack::open_default(&archive.path).expect("open");
        let parsed = pack.read_android_manifest().expect("manifest");
        assert_eq!(parsed.package, "com.example.app");
        assert_eq!(parsed.version_name.as_deref(), Some("1.2.3"));
    }

    #[cfg(feature = "ipa")]
    #[test]
    fn parse_ipa_info_plist() {
        let archive = Scratch::new("app.ipa");
        let plist = r#"
        <plist>
          <dict>
            <key>CFBundleIdentifier</key><string>com.example.bundle</string>
            <key>CFBundleExecutable</key><string>Binary</string>
            <key>CFBundleShortVersionString</key><string>4.2.1</string>
          </dict>
        </plist>
        "#;
        write_zip(
            &archive.path,
            &[(
                "Payload/App.app/Info.plist",
                plist.as_bytes(),
                CompressionMethod::Stored,
            )],
        );
        let pack = OpenPack::open_default(&archive.path).expect("open");
        let parsed = pack.read_info_plist().expect("plist");
        assert_eq!(
            parsed.bundle_identifier.as_deref(),
            Some("com.example.bundle")
        );
    }
}