Skip to main content

provenant/parsers/
android.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5use std::fs::{self, File};
6use std::io::{Cursor, Read};
7use std::path::Path;
8
9use prost::Message;
10use quick_xml::Reader;
11use quick_xml::XmlVersion;
12use quick_xml::events::Event;
13use rusty_axml::{find_nodes_by_type, get_requested_permissions, parse_from_reader};
14use zip::ZipArchive;
15
16use crate::models::{DatasourceId, PackageData, PackageType};
17use crate::parser_warn as warn;
18use crate::parsers::utils::{
19    CappedIterExt, MAX_ITERATION_COUNT, MAX_MANIFEST_SIZE, truncate_field,
20};
21use crate::utils::magic;
22
23use super::PackageParser;
24use super::metadata::ParserMetadata;
25
26const PACKAGE_TYPE: PackageType = PackageType::Android;
27const MAX_ARCHIVE_SIZE: u64 = 100 * 1024 * 1024;
28const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024;
29const MAX_TOTAL_UNCOMPRESSED_SIZE: u64 = 1024 * 1024 * 1024;
30const MAX_COMPRESSION_RATIO: f64 = 100.0;
31const ANDROID_XML_NAMESPACE: &str = "http://schemas.android.com/apk/res/android";
32
33fn default_package_data(datasource_id: DatasourceId) -> PackageData {
34    PackageData {
35        package_type: Some(PACKAGE_TYPE),
36        datasource_id: Some(datasource_id),
37        ..Default::default()
38    }
39}
40
41pub struct AndroidSoongMetadataParser;
42pub struct AndroidManifestParser;
43pub struct AndroidApkParser;
44pub struct AndroidAabParser;
45
46fn looks_like_android_soong_metadata_content(content: &str) -> bool {
47    let mut saw_named_field = false;
48
49    for line in content.lines().take(40) {
50        let trimmed = line.trim();
51
52        if trimmed.is_empty() || trimmed.starts_with('#') {
53            continue;
54        }
55
56        if trimmed.starts_with("//") {
57            return false;
58        }
59
60        if trimmed.starts_with("third_party {")
61            || trimmed.starts_with("third_party{")
62            || trimmed.starts_with("url {")
63            || trimmed.starts_with("url{")
64            || trimmed.starts_with("identifier {")
65            || trimmed.starts_with("identifier{")
66            || trimmed.starts_with("security {")
67            || trimmed.starts_with("security{")
68            || trimmed.starts_with("last_upgrade_date {")
69            || trimmed.starts_with("last_upgrade_date{")
70        {
71            return true;
72        }
73
74        if let Some(value) = trimmed.strip_prefix("license_type:") {
75            let value = value.trim();
76            if !value.is_empty()
77                && value
78                    .chars()
79                    .all(|character| character.is_ascii_uppercase() || character == '_')
80            {
81                return true;
82            }
83        }
84
85        if trimmed.starts_with("name:")
86            || trimmed.starts_with("description:")
87            || trimmed.starts_with("homepage:")
88        {
89            saw_named_field = true;
90        }
91    }
92
93    saw_named_field && content.contains("third_party")
94}
95
96impl PackageParser for AndroidSoongMetadataParser {
97    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
98
99    fn metadata() -> Vec<ParserMetadata> {
100        vec![ParserMetadata {
101            description: "Android Soong METADATA textproto",
102            file_patterns: &["**/METADATA"],
103            package_type: "android",
104            primary_language: "",
105            documentation_url: Some(
106                "https://android.googlesource.com/platform/build/soong/+/refs/heads/main/compliance/project_metadata_proto/project_metadata.proto",
107            ),
108        }]
109    }
110
111    fn is_match(path: &Path) -> bool {
112        if path.file_name().and_then(|name| name.to_str()) != Some("METADATA") {
113            return false;
114        }
115
116        if !path.is_file() {
117            return false;
118        }
119
120        crate::parsers::utils::read_file_to_string(path, Some(MAX_MANIFEST_SIZE))
121            .map(|content| looks_like_android_soong_metadata_content(&content))
122            .unwrap_or(false)
123    }
124
125    fn extract_packages(path: &Path) -> Vec<PackageData> {
126        let content = match crate::parsers::utils::read_file_to_string(path, None) {
127            Ok(content) => content,
128            Err(error) => {
129                warn!(
130                    "Failed to read Android Soong METADATA {:?}: {}",
131                    path, error
132                );
133                return vec![default_package_data(DatasourceId::AndroidSoongMetadata)];
134            }
135        };
136
137        parse_soong_metadata(&content).into_iter().collect()
138    }
139}
140
141impl PackageParser for AndroidManifestParser {
142    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
143
144    fn metadata() -> Vec<ParserMetadata> {
145        vec![ParserMetadata {
146            description: "AndroidManifest.xml metadata (text XML or binary AXML)",
147            file_patterns: &["**/AndroidManifest.xml"],
148            package_type: "android",
149            primary_language: "XML",
150            documentation_url: Some(
151                "https://developer.android.com/guide/topics/manifest/manifest-intro",
152            ),
153        }]
154    }
155
156    fn is_match(path: &Path) -> bool {
157        path.file_name().and_then(|name| name.to_str()) == Some("AndroidManifest.xml")
158    }
159
160    fn extract_packages(path: &Path) -> Vec<PackageData> {
161        let bytes = match read_file_bytes(path, None) {
162            Ok(bytes) => bytes,
163            Err(error) => {
164                warn!("Failed to read AndroidManifest.xml {:?}: {}", path, error);
165                return vec![default_package_data(DatasourceId::AndroidManifestXml)];
166            }
167        };
168
169        parse_manifest_bytes(
170            &bytes,
171            DatasourceId::AndroidManifestXml,
172            "AndroidManifest.xml",
173        )
174        .into_iter()
175        .collect()
176    }
177}
178
179impl PackageParser for AndroidApkParser {
180    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
181
182    fn metadata() -> Vec<ParserMetadata> {
183        vec![ParserMetadata {
184            description: "Android APK archive manifest metadata",
185            file_patterns: &["**/*.apk"],
186            package_type: "android",
187            primary_language: "",
188            documentation_url: Some("https://developer.android.com/build/build-for-release"),
189        }]
190    }
191
192    fn is_match(path: &Path) -> bool {
193        path.extension().and_then(|ext| ext.to_str()) == Some("apk") && magic::is_zip(path)
194    }
195
196    fn extract_packages(path: &Path) -> Vec<PackageData> {
197        match read_best_zip_entry(path, |entry_name| {
198            if entry_name == "AndroidManifest.xml" {
199                Some(0)
200            } else {
201                None
202            }
203        }) {
204            Ok(Some((_, bytes))) => {
205                let package_data = parse_binary_manifest_bytes(&bytes, DatasourceId::AndroidApk)
206                    .unwrap_or_else(|error| {
207                        warn!("Failed to parse APK manifest {:?}: {}", path, error);
208                        default_package_data(DatasourceId::AndroidApk)
209                    });
210                vec![package_data]
211            }
212            Ok(None) => {
213                // A `.apk` with no AndroidManifest.xml is not a usable Android
214                // package (e.g. a deliberately broken test fixture); decline
215                // quietly instead of emitting a scan error.
216                log::debug!("Declining .apk without AndroidManifest.xml {:?}", path);
217                Vec::new()
218            }
219            Err(error) => {
220                // The file claimed a `.apk` extension and a zip magic but is not
221                // a readable archive; decline quietly rather than erroring.
222                log::debug!("Declining unreadable .apk archive {:?}: {}", path, error);
223                Vec::new()
224            }
225        }
226    }
227}
228
229impl PackageParser for AndroidAabParser {
230    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
231
232    fn metadata() -> Vec<ParserMetadata> {
233        vec![ParserMetadata {
234            description: "Android App Bundle (.aab) proto manifest metadata",
235            file_patterns: &["**/*.aab"],
236            package_type: "android",
237            primary_language: "",
238            documentation_url: Some("https://developer.android.com/guide/app-bundle"),
239        }]
240    }
241
242    fn is_match(path: &Path) -> bool {
243        path.extension().and_then(|ext| ext.to_str()) == Some("aab") && magic::is_zip(path)
244    }
245
246    fn extract_packages(path: &Path) -> Vec<PackageData> {
247        match read_best_zip_entry(path, |entry_name| {
248            if entry_name == "base/manifest/AndroidManifest.xml" {
249                Some(0)
250            } else if entry_name.ends_with("/manifest/AndroidManifest.xml") {
251                Some(1)
252            } else {
253                None
254            }
255        }) {
256            Ok(Some((entry_name, bytes))) => {
257                let package_data = parse_proto_manifest_bytes(&bytes).unwrap_or_else(|error| {
258                    warn!(
259                        "Failed to parse AAB manifest {:?} ({}): {}",
260                        path, entry_name, error
261                    );
262                    default_package_data(DatasourceId::AndroidAab)
263                });
264                vec![package_data]
265            }
266            Ok(None) => {
267                // An `.aab` with no proto manifest is not a usable App Bundle;
268                // decline quietly instead of emitting a scan error.
269                log::debug!(
270                    "Declining .aab without proto AndroidManifest.xml {:?}",
271                    path
272                );
273                Vec::new()
274            }
275            Err(error) => {
276                // Claimed a `.aab` extension and zip magic but is not a readable
277                // archive; decline quietly rather than erroring.
278                log::debug!("Declining unreadable .aab archive {:?}: {}", path, error);
279                Vec::new()
280            }
281        }
282    }
283}
284
285fn read_file_bytes(path: &Path, max_size: Option<u64>) -> Result<Vec<u8>, String> {
286    let limit = max_size.unwrap_or(MAX_MANIFEST_SIZE);
287    let metadata =
288        fs::metadata(path).map_err(|error| format!("Cannot stat file {:?}: {}", path, error))?;
289
290    if metadata.len() > limit {
291        return Err(format!(
292            "File {:?} is {} bytes, exceeding the {} byte limit",
293            path,
294            metadata.len(),
295            limit
296        ));
297    }
298
299    let mut file =
300        File::open(path).map_err(|error| format!("Failed to open {:?}: {}", path, error))?;
301    let mut bytes = Vec::with_capacity(metadata.len() as usize);
302    file.read_to_end(&mut bytes)
303        .map_err(|error| format!("Failed to read {:?}: {}", path, error))?;
304    Ok(bytes)
305}
306
307fn parse_soong_metadata(content: &str) -> Option<PackageData> {
308    let parsed = match parse_textproto_map(content) {
309        Ok(parsed) => parsed,
310        Err(error) => {
311            // `METADATA` is matched by filename, so we also see real-world
312            // textproto variants (e.g. legacy `Key: value` chromium METADATA
313            // with unquoted multi-word values) that this conservative parser
314            // does not model. Decline quietly instead of emitting a scan error.
315            log::debug!("Declining unparseable Android Soong METADATA: {error}");
316            return None;
317        }
318    };
319
320    let mut package = default_package_data(DatasourceId::AndroidSoongMetadata);
321    package.name = parsed.get_first_string("name").map(truncate_field);
322    package.description = parsed.get_first_string("description").map(truncate_field);
323
324    if let Some(third_party) = parsed.get_first_map("third_party") {
325        package.version = third_party.get_first_string("version").map(truncate_field);
326
327        let url_entries = third_party
328            .get_all_maps("url")
329            .into_iter()
330            .map(|entry| {
331                let type_ = entry.get_first_string("type").map(truncate_field);
332                let value = entry.get_first_string("value").map(truncate_field);
333                (type_, value)
334            })
335            .collect::<Vec<_>>();
336
337        let homepage_url = third_party.get_first_string("homepage").or_else(|| {
338            url_entries
339                .iter()
340                .find(|(type_, _)| {
341                    type_
342                        .as_deref()
343                        .is_some_and(|type_| type_.eq_ignore_ascii_case("homepage"))
344                })
345                .and_then(|(_, value)| value.clone())
346        });
347        package.homepage_url = homepage_url.map(truncate_field);
348
349        let license_types = third_party
350            .get_all_strings("license_type")
351            .into_iter()
352            .map(truncate_field)
353            .collect::<Vec<_>>();
354        if !license_types.is_empty() {
355            package.extracted_license_statement = Some(license_types.join(", "));
356        }
357
358        let identifiers = third_party
359            .get_all_maps("identifier")
360            .into_iter()
361            .map(|identifier| {
362                let type_ = identifier.get_first_string("type").map(truncate_field);
363                let value = identifier.get_first_string("value").map(truncate_field);
364                let mut object = serde_json::Map::new();
365                if let Some(type_) = type_ {
366                    object.insert("type".to_string(), type_.into());
367                }
368                if let Some(value) = &value {
369                    object.insert("value".to_string(), value.clone().into());
370                }
371
372                if package.vcs_url.is_none()
373                    && let (Some(type_), Some(value)) = (
374                        identifier.get_first_string("type"),
375                        identifier.get_first_string("value"),
376                    )
377                {
378                    let lower_type = type_.to_ascii_lowercase();
379                    if lower_type.contains("git") {
380                        package.vcs_url = Some(truncate_field(value));
381                    } else if lower_type.contains("archive")
382                        || lower_type.contains("tar")
383                        || lower_type.contains("zip")
384                    {
385                        package.download_url = Some(truncate_field(value));
386                    }
387                }
388
389                serde_json::Value::Object(object)
390            })
391            .collect::<Vec<_>>();
392
393        for (type_, value) in &url_entries {
394            let Some(value) = value else {
395                continue;
396            };
397
398            match type_.as_deref().map(str::to_ascii_lowercase).as_deref() {
399                Some("git") if package.vcs_url.is_none() => {
400                    package.vcs_url = Some(value.clone());
401                }
402                Some("archive") if package.download_url.is_none() => {
403                    package.download_url = Some(value.clone());
404                }
405                Some("homepage") if package.homepage_url.is_none() => {
406                    package.homepage_url = Some(value.clone());
407                }
408                _ => {}
409            }
410        }
411
412        let mut extra_data = HashMap::new();
413        if !identifiers.is_empty() {
414            extra_data.insert("identifiers".to_string(), identifiers.into());
415        }
416        if !url_entries.is_empty() {
417            extra_data.insert(
418                "urls".to_string(),
419                url_entries
420                    .iter()
421                    .map(|(type_, value)| {
422                        let mut object = serde_json::Map::new();
423                        if let Some(type_) = type_ {
424                            object.insert("type".to_string(), type_.clone().into());
425                        }
426                        if let Some(value) = value {
427                            object.insert("value".to_string(), value.clone().into());
428                        }
429                        serde_json::Value::Object(object)
430                    })
431                    .collect::<Vec<_>>()
432                    .into(),
433            );
434        }
435
436        if let Some(last_upgrade_date) = third_party.get_first_map("last_upgrade_date") {
437            let year = last_upgrade_date.get_first_string("year");
438            let month = last_upgrade_date.get_first_string("month");
439            let day = last_upgrade_date.get_first_string("day");
440            if let (Some(year), Some(month), Some(day)) = (year, month, day) {
441                let formatted = format!(
442                    "{:04}-{:02}-{:02}",
443                    year.parse::<u32>().unwrap_or_default(),
444                    month.parse::<u32>().unwrap_or_default(),
445                    day.parse::<u32>().unwrap_or_default()
446                );
447                extra_data.insert(
448                    "last_upgrade_date".to_string(),
449                    truncate_field(formatted).into(),
450                );
451            }
452        }
453
454        if let Some(upstream_url) = third_party.get_first_string("url") {
455            extra_data.insert(
456                "upstream_url".to_string(),
457                truncate_field(upstream_url).into(),
458            );
459        }
460
461        if !extra_data.is_empty() {
462            package.extra_data = Some(extra_data);
463        }
464    }
465
466    Some(package)
467}
468
469/// Magic header of a compiled binary Android XML (AXML) resource: a
470/// `RES_XML_TYPE` chunk (type `0x0003`, header size `0x0008`).
471const BINARY_AXML_MAGIC: [u8; 4] = [0x03, 0x00, 0x08, 0x00];
472
473/// Leading byte-order mark some text editors prepend to UTF-8 XML files.
474const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
475
476fn parse_manifest_bytes(
477    bytes: &[u8],
478    datasource_id: DatasourceId,
479    context: &str,
480) -> Option<PackageData> {
481    if looks_like_binary_axml(bytes) {
482        return parse_binary_manifest_bytes(bytes, datasource_id)
483            .map(Some)
484            .unwrap_or_else(|error| {
485                warn!(
486                    "Failed to parse {} as binary Android XML: {}",
487                    context, error
488                );
489                None
490            });
491    }
492
493    if looks_like_text_xml(bytes) {
494        return match parse_text_manifest_bytes(bytes, datasource_id) {
495            Ok(package) => Some(package),
496            Err(error) => {
497                warn!("Failed to parse {} as text XML: {}", context, error);
498                None
499            }
500        };
501    }
502
503    // Neither a compiled AXML chunk nor recognizable text XML: this is not a
504    // manifest we can parse, so decline quietly without a scan error rather
505    // than feeding non-AXML bytes to rusty-axml (which panics on them).
506    log::debug!(
507        "Declining unrecognized {} content (neither binary AXML nor text XML)",
508        context
509    );
510    None
511}
512
513fn looks_like_binary_axml(bytes: &[u8]) -> bool {
514    bytes.starts_with(&BINARY_AXML_MAGIC)
515}
516
517fn looks_like_text_xml(bytes: &[u8]) -> bool {
518    let bytes = bytes.strip_prefix(&UTF8_BOM).unwrap_or(bytes);
519    bytes
520        .iter()
521        .find(|byte| !byte.is_ascii_whitespace())
522        .is_some_and(|byte| *byte == b'<')
523}
524
525fn parse_text_manifest_bytes(
526    bytes: &[u8],
527    datasource_id: DatasourceId,
528) -> Result<PackageData, String> {
529    let content = String::from_utf8(bytes.to_vec())
530        .map_err(|error| format!("Invalid UTF-8 in AndroidManifest.xml: {}", error))?;
531
532    let mut reader = Reader::from_str(&content);
533    reader.config_mut().trim_text(true);
534
535    let mut buf = Vec::new();
536    let mut manifest_attributes = HashMap::new();
537    let mut uses_sdk_attributes = HashMap::new();
538    let mut application_attributes = HashMap::new();
539    let mut requested_permissions = Vec::new();
540    let mut uses_libraries = Vec::new();
541    let mut iteration_count = 0usize;
542
543    loop {
544        iteration_count += 1;
545        if iteration_count > MAX_ITERATION_COUNT {
546            return Err(format!(
547                "Exceeded MAX_ITERATION_COUNT ({}) while parsing AndroidManifest.xml",
548                MAX_ITERATION_COUNT
549            ));
550        }
551
552        match reader.read_event_into(&mut buf) {
553            Ok(Event::Start(event)) | Ok(Event::Empty(event)) => {
554                let name = String::from_utf8_lossy(event.name().as_ref()).into_owned();
555                let attributes = xml_attributes_to_map(&reader, &event)?;
556                match name.as_str() {
557                    "manifest" if manifest_attributes.is_empty() => {
558                        manifest_attributes = attributes
559                    }
560                    "uses-sdk" => uses_sdk_attributes = attributes,
561                    "application" if application_attributes.is_empty() => {
562                        application_attributes = attributes;
563                    }
564                    "uses-permission" | "uses-permission-sdk-23" => {
565                        if let Some(permission) = attributes.get("android:name") {
566                            requested_permissions.push(permission.clone());
567                        }
568                    }
569                    "uses-library" => {
570                        if let Some(library_name) = attributes.get("android:name") {
571                            uses_libraries.push(library_name.clone());
572                        }
573                    }
574                    _ => {}
575                }
576            }
577            Ok(Event::Eof) => break,
578            Err(error) => {
579                return Err(format!(
580                    "XML parse error at position {}: {}",
581                    reader.buffer_position(),
582                    error
583                ));
584            }
585            _ => {}
586        }
587
588        buf.clear();
589    }
590
591    Ok(build_manifest_package_data(
592        datasource_id,
593        &manifest_attributes,
594        &uses_sdk_attributes,
595        &application_attributes,
596        requested_permissions,
597        uses_libraries,
598    ))
599}
600
601fn xml_attributes_to_map(
602    reader: &Reader<&[u8]>,
603    event: &quick_xml::events::BytesStart<'_>,
604) -> Result<HashMap<String, String>, String> {
605    let mut attributes = HashMap::new();
606
607    for attribute in event
608        .attributes()
609        .flatten()
610        .capped("AndroidManifest.xml attributes")
611    {
612        let key = String::from_utf8_lossy(attribute.key.as_ref()).into_owned();
613        let value = attribute
614            .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
615            .map_err(|error| format!("Failed to decode XML attribute {}: {}", key, error))?
616            .into_owned();
617        attributes.insert(key, truncate_field(value));
618    }
619
620    Ok(attributes)
621}
622
623fn parse_binary_manifest_bytes(
624    bytes: &[u8],
625    datasource_id: DatasourceId,
626) -> Result<PackageData, String> {
627    let axml = std::panic::catch_unwind(|| parse_from_reader(Cursor::new(bytes.to_vec())))
628        .map_err(|_| "rusty-axml panicked while parsing binary Android XML".to_string())?
629        .map_err(|error| format!("rusty-axml parse failure: {}", error))?;
630
631    let manifest_attributes =
632        normalize_binary_attributes(axml.root().borrow().attributes().clone());
633    let uses_sdk_attributes = find_nodes_by_type(&axml, "uses-sdk")
634        .into_iter()
635        .next()
636        .map(|node| normalize_binary_attributes(node.borrow().attributes().clone()))
637        .unwrap_or_default();
638    let application_attributes = find_nodes_by_type(&axml, "application")
639        .into_iter()
640        .next()
641        .map(|node| normalize_binary_attributes(node.borrow().attributes().clone()))
642        .unwrap_or_default();
643
644    let requested_permissions = get_requested_permissions(&axml)
645        .into_iter()
646        .map(truncate_field)
647        .collect::<Vec<_>>();
648    let uses_libraries = find_nodes_by_type(&axml, "uses-library")
649        .into_iter()
650        .filter_map(|node| node.borrow().get_attr("android:name").map(str::to_string))
651        .map(truncate_field)
652        .collect::<Vec<_>>();
653
654    Ok(build_manifest_package_data(
655        datasource_id,
656        &manifest_attributes,
657        &uses_sdk_attributes,
658        &application_attributes,
659        requested_permissions,
660        uses_libraries,
661    ))
662}
663
664fn build_manifest_package_data(
665    datasource_id: DatasourceId,
666    manifest_attributes: &HashMap<String, String>,
667    uses_sdk_attributes: &HashMap<String, String>,
668    application_attributes: &HashMap<String, String>,
669    requested_permissions: Vec<String>,
670    uses_libraries: Vec<String>,
671) -> PackageData {
672    let mut package = default_package_data(datasource_id);
673    package.name = manifest_attributes.get("package").cloned();
674    package.version = manifest_attributes
675        .get("android:versionName")
676        .cloned()
677        .or_else(|| manifest_attributes.get("android:versionCode").cloned());
678
679    package.description = application_attributes
680        .get("android:label")
681        .filter(|label| {
682            !label.starts_with('@') && !label.chars().all(|character| character.is_ascii_digit())
683        })
684        .cloned();
685
686    let mut extra_data = HashMap::new();
687    insert_extra(
688        &mut extra_data,
689        "version_code",
690        manifest_attributes.get("android:versionCode"),
691    );
692    insert_extra(
693        &mut extra_data,
694        "compile_sdk_version",
695        manifest_attributes.get("android:compileSdkVersion"),
696    );
697    insert_extra(
698        &mut extra_data,
699        "compile_sdk_version_codename",
700        manifest_attributes.get("android:compileSdkVersionCodename"),
701    );
702    insert_extra(
703        &mut extra_data,
704        "platform_build_version_code",
705        manifest_attributes.get("platformBuildVersionCode"),
706    );
707    insert_extra(
708        &mut extra_data,
709        "platform_build_version_name",
710        manifest_attributes.get("platformBuildVersionName"),
711    );
712    insert_extra(
713        &mut extra_data,
714        "min_sdk_version",
715        uses_sdk_attributes.get("android:minSdkVersion"),
716    );
717    insert_extra(
718        &mut extra_data,
719        "target_sdk_version",
720        uses_sdk_attributes.get("android:targetSdkVersion"),
721    );
722    insert_extra(
723        &mut extra_data,
724        "max_sdk_version",
725        uses_sdk_attributes.get("android:maxSdkVersion"),
726    );
727
728    if !requested_permissions.is_empty() {
729        extra_data.insert(
730            "requested_permissions".to_string(),
731            requested_permissions
732                .into_iter()
733                .map(serde_json::Value::from)
734                .collect::<Vec<_>>()
735                .into(),
736        );
737    }
738    if !uses_libraries.is_empty() {
739        extra_data.insert(
740            "uses_libraries".to_string(),
741            uses_libraries
742                .into_iter()
743                .map(serde_json::Value::from)
744                .collect::<Vec<_>>()
745                .into(),
746        );
747    }
748
749    if !extra_data.is_empty() {
750        package.extra_data = Some(extra_data);
751    }
752
753    package
754}
755
756fn normalize_binary_attributes(attributes: HashMap<String, String>) -> HashMap<String, String> {
757    attributes
758        .into_iter()
759        .map(|(key, value)| (key, normalize_binary_attribute_value(&value)))
760        .collect()
761}
762
763fn normalize_binary_attribute_value(value: &str) -> String {
764    let hex_value = value
765        .strip_prefix("(type 0x10) 0x")
766        .or_else(|| value.strip_prefix("0x"));
767
768    if let Some(hex_value) = hex_value
769        && let Ok(parsed) = u64::from_str_radix(hex_value, 16)
770    {
771        return parsed.to_string();
772    }
773
774    value.to_string()
775}
776
777fn insert_extra(
778    extra_data: &mut HashMap<String, serde_json::Value>,
779    key: &str,
780    value: Option<&String>,
781) {
782    if let Some(value) = value {
783        extra_data.insert(key.to_string(), truncate_field(value.clone()).into());
784    }
785}
786
787fn read_best_zip_entry<F>(
788    path: &Path,
789    mut rank_entry: F,
790) -> Result<Option<(String, Vec<u8>)>, String>
791where
792    F: FnMut(&str) -> Option<u8>,
793{
794    let metadata = fs::metadata(path)
795        .map_err(|error| format!("Failed to stat archive {:?}: {}", path, error))?;
796    if metadata.len() > MAX_ARCHIVE_SIZE {
797        return Err(format!(
798            "Archive {:?} is {} bytes, exceeding the {} byte limit",
799            path,
800            metadata.len(),
801            MAX_ARCHIVE_SIZE
802        ));
803    }
804
805    let file = File::open(path)
806        .map_err(|error| format!("Failed to open archive {:?}: {}", path, error))?;
807    let mut archive = ZipArchive::new(file)
808        .map_err(|error| format!("Failed to parse ZIP archive {:?}: {}", path, error))?;
809
810    let mut total_uncompressed = 0u64;
811    let mut best: Option<(u8, String, Vec<u8>)> = None;
812    let entry_count = archive.len().min(MAX_ITERATION_COUNT);
813
814    if archive.len() > MAX_ITERATION_COUNT {
815        warn!(
816            "Archive {:?} has more than MAX_ITERATION_COUNT ({}) entries; truncating scan",
817            path, MAX_ITERATION_COUNT
818        );
819    }
820
821    for index in 0..entry_count {
822        let mut entry = archive.by_index(index).map_err(|error| {
823            format!(
824                "Failed to read ZIP entry {} in {:?}: {}",
825                index, path, error
826            )
827        })?;
828
829        total_uncompressed = total_uncompressed.saturating_add(entry.size());
830        if total_uncompressed > MAX_TOTAL_UNCOMPRESSED_SIZE {
831            return Err(format!(
832                "Archive {:?} exceeds total uncompressed size limit of {} bytes",
833                path, MAX_TOTAL_UNCOMPRESSED_SIZE
834            ));
835        }
836
837        let entry_name = entry.name().replace('\\', "/");
838        if entry_name.starts_with('/') || entry_name.split('/').any(|segment| segment == "..") {
839            return Err(format!(
840                "Archive entry {} contains a disallowed path",
841                entry_name
842            ));
843        }
844        let Some(rank) = rank_entry(&entry_name) else {
845            continue;
846        };
847
848        if entry.size() > MAX_FILE_SIZE {
849            return Err(format!(
850                "Archive entry {} is {} bytes, exceeding the {} byte limit",
851                entry_name,
852                entry.size(),
853                MAX_FILE_SIZE
854            ));
855        }
856
857        let compressed_size = entry.compressed_size();
858        if compressed_size > 0 {
859            let ratio = entry.size() as f64 / compressed_size as f64;
860            if ratio > MAX_COMPRESSION_RATIO {
861                return Err(format!(
862                    "Archive entry {} has suspicious compression ratio {:.2}:1",
863                    entry_name, ratio
864                ));
865            }
866        }
867
868        let should_replace = match &best {
869            Some((best_rank, _, _)) => rank < *best_rank,
870            None => true,
871        };
872
873        if should_replace {
874            let mut bytes = Vec::with_capacity(entry.size() as usize);
875            entry.read_to_end(&mut bytes).map_err(|error| {
876                format!("Failed to read archive entry {}: {}", entry_name, error)
877            })?;
878            best = Some((rank, entry_name, bytes));
879        }
880    }
881
882    Ok(best.map(|(_, entry_name, bytes)| (entry_name, bytes)))
883}
884
885fn parse_proto_manifest_bytes(bytes: &[u8]) -> Result<PackageData, String> {
886    let node =
887        ProtoXmlNode::decode(bytes).map_err(|error| format!("prost decode failure: {}", error))?;
888    let root_element = node
889        .element()
890        .ok_or_else(|| "Proto manifest root is not an element".to_string())?;
891    if root_element.name != "manifest" {
892        return Err(format!(
893            "Unexpected proto XML root element: {}",
894            root_element.name
895        ));
896    }
897
898    let manifest_attributes = proto_attributes_to_map(&root_element.attribute);
899    let uses_sdk_attributes = root_element
900        .child_elements_named("uses-sdk")
901        .next()
902        .map(|element| proto_attributes_to_map(&element.attribute))
903        .unwrap_or_default();
904    let application_attributes = root_element
905        .child_elements_named("application")
906        .next()
907        .map(|element| proto_attributes_to_map(&element.attribute))
908        .unwrap_or_default();
909    let requested_permissions = root_element
910        .child_elements_named_any(&["uses-permission", "uses-permission-sdk-23"])
911        .filter_map(|element| proto_attributes_to_map(&element.attribute).remove("android:name"))
912        .collect::<Vec<_>>();
913    let uses_libraries = root_element
914        .child_elements_named("uses-library")
915        .filter_map(|element| proto_attributes_to_map(&element.attribute).remove("android:name"))
916        .collect::<Vec<_>>();
917
918    let mut package = build_manifest_package_data(
919        DatasourceId::AndroidAab,
920        &manifest_attributes,
921        &uses_sdk_attributes,
922        &application_attributes,
923        requested_permissions,
924        uses_libraries,
925    );
926
927    if let Some(extra_data) = package.extra_data.as_mut() {
928        extra_data.insert("manifest_encoding".to_string(), "proto".into());
929    } else {
930        package.extra_data = Some(HashMap::from([(
931            "manifest_encoding".to_string(),
932            serde_json::Value::String("proto".to_string()),
933        )]));
934    }
935
936    Ok(package)
937}
938
939fn proto_attributes_to_map(attributes: &[ProtoXmlAttribute]) -> HashMap<String, String> {
940    attributes
941        .iter()
942        .filter_map(|attribute| {
943            let key = proto_attribute_key(attribute)?;
944            let value = proto_attribute_value(attribute)?;
945            Some((key, truncate_field(value)))
946        })
947        .collect()
948}
949
950fn proto_attribute_key(attribute: &ProtoXmlAttribute) -> Option<String> {
951    if attribute.name.is_empty() {
952        return None;
953    }
954
955    if attribute.namespace_uri == ANDROID_XML_NAMESPACE {
956        return Some(format!("android:{}", attribute.name));
957    }
958
959    Some(attribute.name.clone())
960}
961
962fn proto_attribute_value(attribute: &ProtoXmlAttribute) -> Option<String> {
963    if !attribute.value.is_empty() {
964        return Some(attribute.value.clone());
965    }
966
967    attribute
968        .compiled_item
969        .as_ref()
970        .and_then(proto_item_to_string)
971}
972
973fn proto_item_to_string(item: &ProtoItem) -> Option<String> {
974    match &item.value {
975        Some(proto_item::Value::Str(value)) => Some(value.value.clone()),
976        Some(proto_item::Value::RawStr(value)) => Some(value.value.clone()),
977        Some(proto_item::Value::Prim(value)) => proto_primitive_to_string(value),
978        _ => None,
979    }
980}
981
982fn proto_primitive_to_string(primitive: &ProtoPrimitive) -> Option<String> {
983    match &primitive.value {
984        Some(proto_primitive::Value::IntDecimal(value)) => Some(value.to_string()),
985        Some(proto_primitive::Value::IntHexadecimal(value)) => Some(format!("0x{value:x}")),
986        Some(proto_primitive::Value::Boolean(value)) => Some(value.to_string()),
987        Some(proto_primitive::Value::Float(value)) => Some(value.to_string()),
988        Some(proto_primitive::Value::Dimension(value)) => Some(value.to_string()),
989        Some(proto_primitive::Value::Fraction(value)) => Some(value.to_string()),
990        _ => None,
991    }
992}
993
994#[derive(Debug, Clone, Default)]
995struct ProtoMap {
996    fields: HashMap<String, Vec<ProtoValue>>,
997}
998
999#[derive(Debug, Clone)]
1000enum ProtoValue {
1001    Scalar(String),
1002    Map(ProtoMap),
1003}
1004
1005impl ProtoMap {
1006    fn get_first_string(&self, key: &str) -> Option<String> {
1007        self.fields.get(key).and_then(|values| {
1008            values.iter().find_map(|value| match value {
1009                ProtoValue::Scalar(value) => Some(value.clone()),
1010                ProtoValue::Map(_) => None,
1011            })
1012        })
1013    }
1014
1015    fn get_all_strings(&self, key: &str) -> Vec<String> {
1016        self.fields
1017            .get(key)
1018            .into_iter()
1019            .flatten()
1020            .filter_map(|value| match value {
1021                ProtoValue::Scalar(value) => Some(value.clone()),
1022                ProtoValue::Map(_) => None,
1023            })
1024            .collect()
1025    }
1026
1027    fn get_first_map(&self, key: &str) -> Option<ProtoMap> {
1028        self.fields.get(key).and_then(|values| {
1029            values.iter().find_map(|value| match value {
1030                ProtoValue::Map(value) => Some(value.clone()),
1031                ProtoValue::Scalar(_) => None,
1032            })
1033        })
1034    }
1035
1036    fn get_all_maps(&self, key: &str) -> Vec<ProtoMap> {
1037        self.fields
1038            .get(key)
1039            .into_iter()
1040            .flatten()
1041            .filter_map(|value| match value {
1042                ProtoValue::Map(value) => Some(value.clone()),
1043                ProtoValue::Scalar(_) => None,
1044            })
1045            .collect()
1046    }
1047}
1048
1049fn parse_textproto_map(content: &str) -> Result<ProtoMap, String> {
1050    let mut parser = TextProtoParser::new(content)?;
1051    parser.parse_map(false)
1052}
1053
1054struct TextProtoParser {
1055    tokens: Vec<TextProtoToken>,
1056    position: usize,
1057}
1058
1059#[derive(Debug, Clone)]
1060enum TextProtoToken {
1061    Identifier(String),
1062    String(String),
1063    Colon,
1064    LBrace,
1065    RBrace,
1066}
1067
1068impl TextProtoParser {
1069    fn new(content: &str) -> Result<Self, String> {
1070        Ok(Self {
1071            tokens: tokenize_textproto(content)?,
1072            position: 0,
1073        })
1074    }
1075
1076    fn parse_map(&mut self, stop_on_rbrace: bool) -> Result<ProtoMap, String> {
1077        let mut map = ProtoMap::default();
1078
1079        while let Some(token) = self.peek() {
1080            match token {
1081                TextProtoToken::RBrace if stop_on_rbrace => {
1082                    self.position += 1;
1083                    break;
1084                }
1085                TextProtoToken::RBrace => return Err("Unexpected closing brace".to_string()),
1086                TextProtoToken::Identifier(_) => {
1087                    let key = self.expect_identifier()?;
1088                    match self.peek() {
1089                        Some(TextProtoToken::Colon) => {
1090                            self.position += 1;
1091                            match self.peek() {
1092                                Some(TextProtoToken::LBrace) => {
1093                                    self.position += 1;
1094                                    let value = self.parse_map(true)?;
1095                                    map.fields
1096                                        .entry(key)
1097                                        .or_default()
1098                                        .push(ProtoValue::Map(value));
1099                                }
1100                                _ => {
1101                                    let value = self.expect_scalar()?;
1102                                    map.fields
1103                                        .entry(key)
1104                                        .or_default()
1105                                        .push(ProtoValue::Scalar(truncate_field(value)));
1106                                }
1107                            }
1108                        }
1109                        Some(TextProtoToken::LBrace) => {
1110                            self.position += 1;
1111                            let value = self.parse_map(true)?;
1112                            map.fields
1113                                .entry(key)
1114                                .or_default()
1115                                .push(ProtoValue::Map(value));
1116                        }
1117                        Some(other) => {
1118                            return Err(format!("Unexpected token after key: {:?}", other));
1119                        }
1120                        None => return Err("Unexpected end of input after key".to_string()),
1121                    }
1122                }
1123                other => return Err(format!("Unexpected token in textproto: {:?}", other)),
1124            }
1125        }
1126
1127        Ok(map)
1128    }
1129
1130    fn expect_identifier(&mut self) -> Result<String, String> {
1131        match self.next() {
1132            Some(TextProtoToken::Identifier(value)) => Ok(value),
1133            other => Err(format!("Expected identifier, found {:?}", other)),
1134        }
1135    }
1136
1137    fn expect_scalar(&mut self) -> Result<String, String> {
1138        match self.next() {
1139            Some(TextProtoToken::String(mut value)) => {
1140                while matches!(self.peek(), Some(TextProtoToken::String(_))) {
1141                    if let Some(TextProtoToken::String(next)) = self.next() {
1142                        value.push_str(&next);
1143                    }
1144                }
1145                Ok(value)
1146            }
1147            Some(TextProtoToken::Identifier(value)) => Ok(value),
1148            other => Err(format!("Expected scalar value, found {:?}", other)),
1149        }
1150    }
1151
1152    fn peek(&self) -> Option<&TextProtoToken> {
1153        self.tokens.get(self.position)
1154    }
1155
1156    fn next(&mut self) -> Option<TextProtoToken> {
1157        let token = self.tokens.get(self.position).cloned();
1158        if token.is_some() {
1159            self.position += 1;
1160        }
1161        token
1162    }
1163}
1164
1165fn tokenize_textproto(content: &str) -> Result<Vec<TextProtoToken>, String> {
1166    let mut tokens = Vec::new();
1167    let chars = content.chars().collect::<Vec<_>>();
1168    let mut index = 0usize;
1169
1170    while index < chars.len() {
1171        match chars[index] {
1172            '{' => {
1173                tokens.push(TextProtoToken::LBrace);
1174                index += 1;
1175            }
1176            '}' => {
1177                tokens.push(TextProtoToken::RBrace);
1178                index += 1;
1179            }
1180            ':' => {
1181                tokens.push(TextProtoToken::Colon);
1182                index += 1;
1183            }
1184            '"' => {
1185                index += 1;
1186                let mut value = String::new();
1187                while index < chars.len() {
1188                    match chars[index] {
1189                        '\\' if index + 1 < chars.len() => {
1190                            index += 1;
1191                            value.push(chars[index]);
1192                            index += 1;
1193                        }
1194                        '"' => {
1195                            index += 1;
1196                            break;
1197                        }
1198                        character => {
1199                            value.push(character);
1200                            index += 1;
1201                        }
1202                    }
1203                }
1204                tokens.push(TextProtoToken::String(value));
1205            }
1206            '#' => {
1207                while index < chars.len() && chars[index] != '\n' {
1208                    index += 1;
1209                }
1210            }
1211            '/' if index + 1 < chars.len() && chars[index + 1] == '/' => {
1212                index += 2;
1213                while index < chars.len() && chars[index] != '\n' {
1214                    index += 1;
1215                }
1216            }
1217            character if character.is_ascii_whitespace() => index += 1,
1218            _ => {
1219                let start = index;
1220                while index < chars.len() {
1221                    let character = chars[index];
1222                    let starts_comment =
1223                        character == '/' && index + 1 < chars.len() && chars[index + 1] == '/';
1224
1225                    if character.is_ascii_whitespace()
1226                        || matches!(character, '{' | '}' | ':' | '#')
1227                        || starts_comment
1228                    {
1229                        break;
1230                    }
1231
1232                    index += 1;
1233                }
1234
1235                let token = chars[start..index].iter().collect::<String>();
1236                if token.is_empty() {
1237                    return Err("Encountered empty textproto token".to_string());
1238                }
1239                tokens.push(TextProtoToken::Identifier(token));
1240            }
1241        }
1242    }
1243
1244    Ok(tokens)
1245}
1246
1247#[derive(Clone, PartialEq, Message)]
1248pub struct ProtoSourcePosition {
1249    #[prost(uint32, tag = "1")]
1250    pub line_number: u32,
1251    #[prost(uint32, tag = "2")]
1252    pub column_number: u32,
1253}
1254
1255#[derive(Clone, PartialEq, Message)]
1256pub struct ProtoXmlNode {
1257    #[prost(oneof = "proto_xml_node::Node", tags = "1, 2")]
1258    pub node: Option<proto_xml_node::Node>,
1259    #[prost(message, optional, tag = "3")]
1260    pub source: Option<ProtoSourcePosition>,
1261}
1262
1263impl ProtoXmlNode {
1264    fn element(&self) -> Option<&ProtoXmlElement> {
1265        match &self.node {
1266            Some(proto_xml_node::Node::Element(element)) => Some(element),
1267            _ => None,
1268        }
1269    }
1270}
1271
1272pub mod proto_xml_node {
1273    use super::ProtoXmlElement;
1274    use prost::Oneof;
1275
1276    #[derive(Clone, PartialEq, Oneof)]
1277    pub enum Node {
1278        #[prost(message, tag = "1")]
1279        Element(ProtoXmlElement),
1280        #[prost(string, tag = "2")]
1281        Text(String),
1282    }
1283}
1284
1285#[derive(Clone, PartialEq, Message)]
1286pub struct ProtoXmlElement {
1287    #[prost(message, repeated, tag = "1")]
1288    pub namespace_declaration: Vec<ProtoXmlNamespace>,
1289    #[prost(string, tag = "2")]
1290    pub namespace_uri: String,
1291    #[prost(string, tag = "3")]
1292    pub name: String,
1293    #[prost(message, repeated, tag = "4")]
1294    pub attribute: Vec<ProtoXmlAttribute>,
1295    #[prost(message, repeated, tag = "5")]
1296    pub child: Vec<ProtoXmlNode>,
1297}
1298
1299impl ProtoXmlElement {
1300    fn child_elements_named<'a>(
1301        &'a self,
1302        name: &'a str,
1303    ) -> impl Iterator<Item = &'a ProtoXmlElement> {
1304        self.child
1305            .iter()
1306            .filter_map(ProtoXmlNode::element)
1307            .filter(move |element| element.name == name)
1308    }
1309
1310    fn child_elements_named_any<'a>(
1311        &'a self,
1312        names: &'a [&'a str],
1313    ) -> impl Iterator<Item = &'a ProtoXmlElement> {
1314        self.child
1315            .iter()
1316            .filter_map(ProtoXmlNode::element)
1317            .filter(move |element| names.contains(&element.name.as_str()))
1318    }
1319}
1320
1321#[derive(Clone, PartialEq, Message)]
1322pub struct ProtoXmlNamespace {
1323    #[prost(string, tag = "1")]
1324    pub prefix: String,
1325    #[prost(string, tag = "2")]
1326    pub uri: String,
1327    #[prost(message, optional, tag = "3")]
1328    pub source: Option<ProtoSourcePosition>,
1329}
1330
1331#[derive(Clone, PartialEq, Message)]
1332pub struct ProtoXmlAttribute {
1333    #[prost(string, tag = "1")]
1334    pub namespace_uri: String,
1335    #[prost(string, tag = "2")]
1336    pub name: String,
1337    #[prost(string, tag = "3")]
1338    pub value: String,
1339    #[prost(message, optional, tag = "4")]
1340    pub source: Option<ProtoSourcePosition>,
1341    #[prost(uint32, tag = "5")]
1342    pub resource_id: u32,
1343    #[prost(message, optional, tag = "6")]
1344    pub compiled_item: Option<ProtoItem>,
1345}
1346
1347#[derive(Clone, PartialEq, Message)]
1348pub struct ProtoItem {
1349    #[prost(oneof = "proto_item::Value", tags = "2, 3, 7")]
1350    pub value: Option<proto_item::Value>,
1351    #[prost(uint32, tag = "8")]
1352    pub flag_status: u32,
1353    #[prost(bool, tag = "9")]
1354    pub flag_negated: bool,
1355    #[prost(string, tag = "10")]
1356    pub flag_name: String,
1357}
1358
1359pub mod proto_item {
1360    use super::{ProtoPrimitive, ProtoRawStringValue, ProtoStringValue};
1361    use prost::Oneof;
1362
1363    #[derive(Clone, PartialEq, Oneof)]
1364    pub enum Value {
1365        #[prost(message, tag = "2")]
1366        Str(ProtoStringValue),
1367        #[prost(message, tag = "3")]
1368        RawStr(ProtoRawStringValue),
1369        #[prost(message, tag = "7")]
1370        Prim(ProtoPrimitive),
1371    }
1372}
1373
1374#[derive(Clone, PartialEq, Message)]
1375pub struct ProtoStringValue {
1376    #[prost(string, tag = "1")]
1377    pub value: String,
1378}
1379
1380#[derive(Clone, PartialEq, Message)]
1381pub struct ProtoRawStringValue {
1382    #[prost(string, tag = "1")]
1383    pub value: String,
1384}
1385
1386#[derive(Clone, PartialEq, Message)]
1387pub struct ProtoPrimitive {
1388    #[prost(oneof = "proto_primitive::Value", tags = "3, 6, 7, 8, 13, 14")]
1389    pub value: Option<proto_primitive::Value>,
1390}
1391
1392pub mod proto_primitive {
1393    use prost::Oneof;
1394
1395    #[derive(Clone, PartialEq, Oneof)]
1396    pub enum Value {
1397        #[prost(float, tag = "3")]
1398        Float(f32),
1399        #[prost(int32, tag = "6")]
1400        IntDecimal(i32),
1401        #[prost(uint32, tag = "7")]
1402        IntHexadecimal(u32),
1403        #[prost(bool, tag = "8")]
1404        Boolean(bool),
1405        #[prost(uint32, tag = "13")]
1406        Dimension(u32),
1407        #[prost(uint32, tag = "14")]
1408        Fraction(u32),
1409    }
1410}