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