1use 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}