1use crate::resources::{plain_value, resolve_simple};
12use crate::text::value;
13use crate::{Engine, Result};
14use asc_arsc::{EntryValue, ResTable, TYPE_INT_BOOLEAN, TYPE_STRING};
15use asc_axml::{Attribute, Element};
16use serde_json::{Value as Json, json};
17
18const SDK_EXPLICIT_EXPORTED: i64 = 31;
21const SDK_PROVIDER_PRIVATE: i64 = 17;
23const SDK_CLEARTEXT_OFF: i64 = 28;
25const FILE_PROVIDER_PATHS: &str = "android.support.FILE_PROVIDER_PATHS";
26
27#[derive(Debug, Clone, Default, PartialEq)]
29#[non_exhaustive]
30pub struct ManifestSummary {
31 pub package: String,
33 pub version_code: Option<String>,
35 pub version_name: Option<String>,
37 pub min_sdk: Option<i64>,
39 pub target_sdk: Option<i64>,
41 pub shared_user_id: Option<String>,
43 pub application: Application,
45 pub uses_permissions: Vec<String>,
47 pub permissions: Vec<Permission>,
49 pub components: Vec<Component>,
51 pub network_security: Vec<String>,
53 pub warnings: Vec<String>,
55}
56
57#[derive(Debug, Clone, Default, PartialEq)]
59#[non_exhaustive]
60pub struct Application {
61 pub class: Option<String>,
63 pub debuggable: bool,
65 pub allow_backup: bool,
67 pub test_only: bool,
69 pub uses_cleartext_traffic: Option<bool>,
71 pub cleartext_effective: (bool, &'static str),
73 pub network_security_config: Option<String>,
75 pub permission: Option<String>,
77 pub full_backup_content: Option<String>,
79 pub data_extraction_rules: Option<String>,
81}
82
83#[derive(Debug, Clone, Default, PartialEq)]
85#[non_exhaustive]
86pub struct Permission {
87 pub name: String,
89 pub protection_level: String,
91}
92
93#[derive(Debug, Clone, Default, PartialEq)]
95#[non_exhaustive]
96pub struct Component {
97 pub kind: String,
99 pub class: String,
101 pub target: Option<String>,
103 pub exported: bool,
105 pub exported_reason: String,
109 pub enabled: bool,
111 pub permission: Option<String>,
113 pub permission_from_app: bool,
115 pub process: Option<String>,
117 pub authorities: Vec<String>,
119 pub grant_uri_permissions: bool,
121 pub read_permission: Option<String>,
123 pub write_permission: Option<String>,
125 pub intent_filters: Vec<IntentFilter>,
127 pub meta_data: Vec<(String, String)>,
129 pub provider_paths: Vec<String>,
131}
132
133#[derive(Debug, Clone, Default, PartialEq)]
135#[non_exhaustive]
136pub struct IntentFilter {
137 pub actions: Vec<String>,
139 pub categories: Vec<String>,
141 pub data: Vec<DataSpec>,
143}
144
145#[derive(Debug, Clone, Default, PartialEq)]
147#[non_exhaustive]
148pub struct DataSpec {
149 pub scheme: Option<String>,
151 pub host: Option<String>,
153 pub port: Option<String>,
155 pub path: Option<(String, String)>,
158 pub mime_type: Option<String>,
160}
161
162impl IntentFilter {
163 pub fn deep_links(&self) -> Vec<String> {
166 if !self.actions.iter().any(|a| a == "android.intent.action.VIEW") {
167 return Vec::new();
168 }
169 let pick = |f: fn(&DataSpec) -> Option<String>| -> Vec<String> {
170 let mut v: Vec<String> = self.data.iter().filter_map(f).collect();
171 v.dedup();
172 v
173 };
174 let schemes = pick(|d| d.scheme.clone());
175 let hosts = pick(|d| {
176 d.host.clone().map(|h| match &d.port {
177 Some(p) => format!("{h}:{p}"),
178 None => h,
179 })
180 });
181 let paths = pick(|d| d.path.as_ref().map(|(k, v)| if k == "pathPrefix" { format!("{v}*") } else { v.clone() }));
182 let mut out = Vec::new();
183 for s in &schemes {
184 if !matches!(s.as_str(), "http" | "https") && hosts.is_empty() {
185 out.push(format!("{s}://*"));
186 continue;
187 }
188 for h in if hosts.is_empty() { vec!["*".to_string()] } else { hosts.clone() } {
189 if paths.is_empty() {
190 out.push(format!("{s}://{h}"));
191 }
192 for p in &paths {
193 let sep = if p.starts_with('/') { "" } else { "/" };
194 out.push(format!("{s}://{h}{sep}{p}"));
195 }
196 }
197 }
198 out
199 }
200
201 fn line(&self) -> String {
202 let list = |vs: &[String]| vs.iter().map(|v| value(v)).collect::<Vec<_>>().join(",");
203 let mut parts = Vec::new();
204 if !self.actions.is_empty() {
205 parts.push(format!("action={}", list(&self.actions)));
206 }
207 if !self.categories.is_empty() {
208 parts.push(format!("category={}", list(&self.categories)));
209 }
210 for d in &self.data {
211 let mut s = String::new();
212 if let Some(v) = &d.scheme {
213 s.push_str(&format!("scheme={} ", value(v)));
214 }
215 if let Some(v) = &d.host {
216 s.push_str(&format!("host={} ", value(v)));
217 }
218 if let Some(v) = &d.port {
219 s.push_str(&format!("port={} ", value(v)));
220 }
221 if let Some((k, v)) = &d.path {
222 s.push_str(&format!("{k}={} ", value(v)));
223 }
224 if let Some(v) = &d.mime_type {
225 s.push_str(&format!("mimeType={} ", value(v)));
226 }
227 parts.push(format!("data({})", s.trim_end()));
228 }
229 parts.join(" ")
230 }
231
232 fn json(&self) -> Json {
233 json!({
234 "actions": self.actions,
235 "categories": self.categories,
236 "data": self.data.iter().map(|d| json!({
237 "scheme": d.scheme, "host": d.host, "port": d.port,
238 "path_kind": d.path.as_ref().map(|p| &p.0), "path": d.path.as_ref().map(|p| &p.1),
239 "mime_type": d.mime_type,
240 })).collect::<Vec<_>>(),
241 })
242 }
243}
244
245pub(crate) fn protection_level_name(level: u32) -> String {
247 let base = match level & 0xf {
248 0 => "normal".to_string(),
249 1 => "dangerous".to_string(),
250 2 => "signature".to_string(),
251 3 => "signatureOrSystem".to_string(),
252 4 => "internal".to_string(),
253 b => format!("base{b}"),
254 };
255 const FLAGS: [(u32, &str); 18] = [
256 (0x10, "privileged"),
257 (0x20, "development"),
258 (0x40, "appop"),
259 (0x80, "pre23"),
260 (0x100, "installer"),
261 (0x200, "verifier"),
262 (0x400, "preinstalled"),
263 (0x800, "setup"),
264 (0x1000, "instant"),
265 (0x2000, "runtime"),
266 (0x4000, "oem"),
267 (0x8000, "vendorPrivileged"),
268 (0x10000, "textClassifier"),
269 (0x20000, "configurator"),
270 (0x40000, "incidentReportApprover"),
271 (0x80000, "appPredictor"),
272 (0x100000, "module"),
273 (0x200000, "companion"),
274 ];
275 let mut out = base;
276 let mut rest = level & !0xf;
277 for (bit, name) in FLAGS {
278 if rest & bit != 0 {
279 out.push('|');
280 out.push_str(name);
281 rest &= !bit;
282 }
283 }
284 if rest != 0 {
285 out.push_str(&format!("|0x{rest:x}"));
286 }
287 out
288}
289
290struct Reader<'a> {
292 table: Option<&'a ResTable>,
293 warnings: Vec<String>,
294}
295
296impl Reader<'_> {
297 fn string(&mut self, a: &Attribute) -> String {
300 if let (Some(id), Some(t)) = (a.reference(), self.table) {
301 match resolve_simple(t, id) {
302 Some(v) => return plain_value(t, &v),
303 None => self.warnings.push(format!("{}={} does not resolve to a value", a.name, a.value)),
304 }
305 }
306 a.value.clone()
307 }
308
309 fn str_attr(&mut self, el: &Element, name: &str) -> Option<String> {
310 el.android(name).map(|a| self.string(a))
311 }
312
313 fn bool_attr(&mut self, el: &Element, name: &str) -> Option<(bool, Option<String>)> {
316 let a = el.android(name)?;
317 if let Some(b) = a.as_bool() {
318 return Some((b, None));
319 }
320 let (Some(id), Some(t)) = (a.reference(), self.table) else {
321 self.warnings.push(format!("{name}={} is not a boolean", a.value));
322 return None;
323 };
324 let variants: Vec<(String, bool)> = t
325 .values(id)
326 .into_iter()
327 .filter_map(|v| match v.value {
328 EntryValue::Simple(x) if x.kind == TYPE_INT_BOOLEAN => Some((v.config.to_string(), x.data != 0)),
329 EntryValue::Simple(x) if x.kind == TYPE_STRING => {
330 Some((v.config.to_string(), t.string(x.data).is_some_and(|s| s == "true")))
331 }
332 _ => None,
333 })
334 .collect();
335 let resolved = resolve_simple(t, id).and_then(|v| match v.kind {
336 TYPE_INT_BOOLEAN => Some(v.data != 0),
337 TYPE_STRING => t.string(v.data).map(|s| s == "true"),
338 _ => None,
339 });
340 let Some(b) = resolved else {
341 self.warnings.push(format!("{name}={} does not resolve to a boolean", a.value));
342 return None;
343 };
344 let differs = variants.iter().any(|(_, v)| *v != variants[0].1);
345 let note = differs.then(|| {
346 let list: Vec<String> = variants.iter().map(|(c, v)| format!("{}={v}", if c.is_empty() { "default" } else { c })).collect();
347 format!("{} varies: {}", a.value, list.join(","))
348 });
349 Some((b, note))
350 }
351
352 fn int_attr(&mut self, el: &Element, name: &str) -> Option<i64> {
353 let a = el.android(name)?;
354 if let Some(i) = a.as_int() {
355 return Some(i);
356 }
357 if let (Some(id), Some(t)) = (a.reference(), self.table)
358 && let Some(v) = resolve_simple(t, id)
359 {
360 return match v.kind {
361 TYPE_STRING => t.string(v.data).and_then(|s| s.trim().parse().ok()),
362 _ => Some(v.data as i32 as i64),
363 };
364 }
365 a.value.parse().ok()
367 }
368
369 fn xml_path(&mut self, a: &Attribute) -> Option<String> {
371 let (id, t) = (a.reference()?, self.table?);
372 match resolve_simple(t, id) {
373 Some(v) if v.kind == TYPE_STRING => t.string(v.data).map(|s| s.into_owned()),
374 _ => {
375 self.warnings.push(format!("{}={} is not a file resource", a.name, a.value));
376 None
377 }
378 }
379 }
380}
381
382fn class_name(package: &str, name: &str) -> String {
384 if let Some(rest) = name.strip_prefix('.') {
385 format!("{package}.{rest}")
386 } else if !name.contains('.') && !package.is_empty() {
387 format!("{package}.{name}")
388 } else {
389 name.to_string()
390 }
391}
392
393impl Engine {
394 pub fn manifest_summary(&self) -> Result<ManifestSummary> {
406 let apk = self.open()?;
407 let data = apk.read("AndroidManifest.xml")?;
408 let table = self.resources()?;
409 let resolver = table.as_deref().map(|t| t as &dyn asc_axml::Resolver);
410 let root = asc_axml::parse_tree(&data, resolver)?;
411 let mut r = Reader { table: table.as_deref(), warnings: Vec::new() };
412 let mut s = ManifestSummary {
413 package: root.plain("package").map(|a| a.value.clone()).unwrap_or_default(),
414 version_code: r.str_attr(&root, "versionCode"),
415 version_name: r.str_attr(&root, "versionName"),
416 shared_user_id: r.str_attr(&root, "sharedUserId"),
417 ..ManifestSummary::default()
418 };
419 if table.is_none() {
420 s.warnings.push("no resources.arsc: references are not resolved".into());
421 }
422 for sdk in root.children_named("uses-sdk") {
423 s.min_sdk = r.int_attr(sdk, "minSdkVersion").or(s.min_sdk);
424 s.target_sdk = r.int_attr(sdk, "targetSdkVersion").or(s.target_sdk);
425 }
426 let target = s.target_sdk.or(s.min_sdk).unwrap_or(1);
428 for el in &root.children {
429 match el.name.as_str() {
430 "uses-permission" | "uses-permission-sdk-23" | "uses-permission-sdk-m" => {
431 if let Some(n) = r.str_attr(el, "name") {
432 s.uses_permissions.push(n);
433 }
434 }
435 "permission" => {
436 let level = el.android("protectionLevel").map_or(0, |a| a.data);
437 s.permissions.push(Permission {
438 name: r.str_attr(el, "name").unwrap_or_default(),
439 protection_level: protection_level_name(level),
440 });
441 }
442 _ => {}
443 }
444 }
445 if let Some(app) = root.children_named("application").next() {
446 let a = &mut s.application;
447 a.class = r.str_attr(app, "name").map(|n| class_name(&s.package, &n));
448 a.debuggable = r.bool_attr(app, "debuggable").is_some_and(|b| b.0);
449 a.allow_backup = r.bool_attr(app, "allowBackup").is_none_or(|b| b.0);
450 a.test_only = r.bool_attr(app, "testOnly").is_some_and(|b| b.0);
451 a.uses_cleartext_traffic = r.bool_attr(app, "usesCleartextTraffic").map(|b| b.0);
452 a.permission = r.str_attr(app, "permission");
453 a.full_backup_content = app.android("fullBackupContent").map(|x| r.xml_path(x).unwrap_or_else(|| x.value.clone()));
454 a.data_extraction_rules = app.android("dataExtractionRules").map(|x| r.xml_path(x).unwrap_or_else(|| x.value.clone()));
455 a.network_security_config = app.android("networkSecurityConfig").and_then(|x| r.xml_path(x));
456 let mut nsc_cleartext = None;
457 if let Some(path) = a.network_security_config.clone() {
458 match apk.read(&path).map_err(crate::EngineError::from).and_then(|d| Ok(asc_axml::parse_tree(&d, resolver)?)) {
459 Ok(doc) => nsc_cleartext = network_security(&doc, table.as_deref(), &mut s.network_security),
460 Err(e) => r.warnings.push(format!("networkSecurityConfig {path}: {e}")),
461 }
462 }
463 a.cleartext_effective = match (nsc_cleartext, a.uses_cleartext_traffic) {
464 (Some(b), _) => (b, "networkSecurityConfig"),
465 (None, Some(b)) => (b, "usesCleartextTraffic"),
466 (None, None) => (target < SDK_CLEARTEXT_OFF, "targetSdk default"),
467 };
468 let app_permission = a.permission.clone();
469 for el in &app.children {
470 let kind = el.name.as_str();
471 if !matches!(kind, "activity" | "activity-alias" | "service" | "receiver" | "provider") {
472 continue;
473 }
474 let mut c = Component {
475 kind: kind.to_string(),
476 class: class_name(&s.package, &r.str_attr(el, "name").unwrap_or_default()),
477 target: r.str_attr(el, "targetActivity").map(|n| class_name(&s.package, &n)),
478 enabled: r.bool_attr(el, "enabled").is_none_or(|b| b.0),
479 process: r.str_attr(el, "process"),
480 ..Component::default()
481 };
482 c.permission = r.str_attr(el, "permission");
483 if c.permission.is_none() && app_permission.is_some() {
484 c.permission = app_permission.clone();
485 c.permission_from_app = true;
486 }
487 for f in el.children_named("intent-filter") {
488 let mut filter = IntentFilter::default();
489 for x in &f.children {
490 match x.name.as_str() {
491 "action" => filter.actions.extend(r.str_attr(x, "name")),
492 "category" => filter.categories.extend(r.str_attr(x, "name")),
493 "data" => {
494 let mut d = DataSpec {
495 scheme: r.str_attr(x, "scheme"),
496 host: r.str_attr(x, "host"),
497 port: r.str_attr(x, "port"),
498 mime_type: r.str_attr(x, "mimeType"),
499 path: None,
500 };
501 for k in ["path", "pathPrefix", "pathPattern", "pathAdvancedPattern", "pathSuffix"] {
502 if let Some(v) = r.str_attr(x, k) {
503 d.path = Some((k.to_string(), v));
504 }
505 }
506 filter.data.push(d);
507 }
508 _ => {}
509 }
510 }
511 c.intent_filters.push(filter);
512 }
513 let (exported, reason) = match r.bool_attr(el, "exported") {
514 Some((b, note)) => (b, note.map_or("explicit".to_string(), |n| format!("explicit, {n}"))),
515 None if !c.intent_filters.is_empty() && target < SDK_EXPLICIT_EXPORTED => (true, "intent-filter".to_string()),
516 None if !c.intent_filters.is_empty() => (false, "missing (install fails on targetSdk>=31)".to_string()),
517 None if kind == "provider" && target < SDK_PROVIDER_PRIVATE => (true, "provider-default".to_string()),
518 None => (false, "default".to_string()),
519 };
520 c.exported = exported;
521 c.exported_reason = reason;
522 if kind == "provider" {
523 c.authorities = r.str_attr(el, "authorities").map(|a| a.split(';').map(str::to_string).collect()).unwrap_or_default();
524 c.grant_uri_permissions = r.bool_attr(el, "grantUriPermissions").is_some_and(|b| b.0);
525 c.read_permission = r.str_attr(el, "readPermission");
526 c.write_permission = r.str_attr(el, "writePermission");
527 }
528 for m in el.children_named("meta-data") {
529 let name = r.str_attr(m, "name").unwrap_or_default();
530 let value = m.android("value").or(m.android("resource")).map(|a| r.string(a));
531 if name == FILE_PROVIDER_PATHS
532 && let Some(path) = m.android("resource").and_then(|a| r.xml_path(a))
533 {
534 match apk.read(&path).map_err(crate::EngineError::from).and_then(|d| Ok(asc_axml::parse_tree(&d, resolver)?)) {
535 Ok(doc) => c.provider_paths = provider_paths(&doc),
536 Err(e) => r.warnings.push(format!("{FILE_PROVIDER_PATHS} {path}: {e}")),
537 }
538 }
539 c.meta_data.push((name, value.unwrap_or_default()));
540 }
541 s.components.push(c);
542 }
543 }
544 s.warnings.extend(r.warnings);
545 Ok(s)
546 }
547}
548
549fn network_security(doc: &Element, table: Option<&ResTable>, out: &mut Vec<String>) -> Option<bool> {
552 let cleartext = |el: &Element| el.plain("cleartextTrafficPermitted").and_then(|a| a.as_bool());
553 let anchors = |el: &Element| -> String {
554 let list: Vec<String> = el
555 .children_named("trust-anchors")
556 .flat_map(|t| t.children_named("certificates"))
557 .map(|c| {
558 let src = c.plain("src").map_or(String::new(), |a| match (a.reference(), table) {
559 (Some(id), Some(t)) => format!("@{}", t.ref_name(id)),
560 _ => a.value.clone(),
561 });
562 match c.plain("overridePins").and_then(|a| a.as_bool()) {
563 Some(true) => format!("{src}(overridePins)"),
564 _ => src,
565 }
566 })
567 .collect();
568 list.join(",")
569 };
570 let mut base_cleartext = None;
571 for el in &doc.children {
572 match el.name.as_str() {
573 "base-config" => {
574 base_cleartext = cleartext(el);
575 out.push(format!("base-config cleartextTrafficPermitted={} trust-anchors={}", fmt_opt(cleartext(el)), anchors(el)));
576 }
577 "debug-overrides" => out.push(format!("debug-overrides trust-anchors={}", anchors(el))),
578 "domain-config" => domain_config(el, &cleartext, &anchors, out),
579 _ => {}
580 }
581 }
582 base_cleartext
583}
584
585fn domain_config(el: &Element, cleartext: &dyn Fn(&Element) -> Option<bool>, anchors: &dyn Fn(&Element) -> String, out: &mut Vec<String>) {
586 let domains: Vec<String> = el
587 .children_named("domain")
588 .map(|d| {
589 let sub = d.plain("includeSubdomains").and_then(|a| a.as_bool()).unwrap_or(false);
590 if sub { format!("*.{}", d.text) } else { d.text.clone() }
591 })
592 .collect();
593 let pins: usize = el.children_named("pin-set").map(|p| p.children_named("pin").count()).sum();
594 out.push(format!(
595 "domain-config domains={} cleartextTrafficPermitted={} trust-anchors={} pins={pins}",
596 domains.join(","),
597 fmt_opt(cleartext(el)),
598 anchors(el)
599 ));
600 for nested in el.children_named("domain-config") {
601 domain_config(nested, cleartext, anchors, out);
602 }
603}
604
605fn fmt_opt(b: Option<bool>) -> String {
606 b.map_or("-".to_string(), |b| b.to_string())
607}
608
609fn provider_paths(doc: &Element) -> Vec<String> {
611 doc.children
612 .iter()
613 .map(|p| {
614 let attr = |n: &str| p.plain(n).map_or("-".to_string(), |a| value(&a.value).into_owned());
615 format!("{} name={} path={}", p.name, attr("name"), attr("path"))
616 })
617 .collect()
618}
619
620impl Component {
621 fn line(&self) -> String {
622 let mut s = format!("{} {} exported={} ({})", self.kind, value(&self.class), self.exported, self.exported_reason);
623 if !self.enabled {
624 s.push_str(" enabled=false");
625 }
626 if let Some(t) = &self.target {
627 s.push_str(&format!(" target={}", value(t)));
628 }
629 if let Some(p) = &self.permission {
630 s.push_str(&format!(" permission={}{}", value(p), if self.permission_from_app { "(application)" } else { "" }));
631 }
632 if self.kind == "provider" {
633 s.push_str(&format!(" authorities={} grantUriPermissions={}", self.authorities(), self.grant_uri_permissions));
634 if let Some(p) = &self.read_permission {
635 s.push_str(&format!(" readPermission={}", value(p)));
636 }
637 if let Some(p) = &self.write_permission {
638 s.push_str(&format!(" writePermission={}", value(p)));
639 }
640 }
641 if let Some(p) = &self.process {
642 s.push_str(&format!(" process={}", value(p)));
643 }
644 s
645 }
646
647 fn authorities(&self) -> String {
649 self.authorities.iter().map(|a| value(a)).collect::<Vec<_>>().join(";")
650 }
651
652 pub fn json(&self) -> Json {
654 json!({
655 "kind": self.kind,
656 "class": self.class,
657 "target": self.target,
658 "exported": self.exported,
659 "exported_reason": self.exported_reason,
660 "enabled": self.enabled,
661 "permission": self.permission,
662 "permission_from_application": self.permission_from_app,
663 "process": self.process,
664 "authorities": self.authorities,
665 "grant_uri_permissions": self.grant_uri_permissions,
666 "read_permission": self.read_permission,
667 "write_permission": self.write_permission,
668 "intent_filters": self.intent_filters.iter().map(IntentFilter::json).collect::<Vec<_>>(),
669 "deep_links": self.deep_links(),
670 "meta_data": self.meta_data.iter().map(|(k, v)| json!({ "name": k, "value": v })).collect::<Vec<_>>(),
671 "provider_paths": self.provider_paths,
672 })
673 }
674
675 pub fn deep_links(&self) -> Vec<String> {
677 let mut v: Vec<String> = self.intent_filters.iter().flat_map(IntentFilter::deep_links).collect();
678 v.dedup();
679 v
680 }
681}
682
683impl ManifestSummary {
684 pub fn lines(&self) -> Vec<String> {
690 let opt = |o: &Option<String>| o.as_deref().map_or("-".to_string(), |v| value(v).into_owned());
691 let opt_i = |o: Option<i64>| o.map_or("-".to_string(), |v| v.to_string());
692 let mut out = vec![format!(
693 "package {} versionCode={} versionName={} minSdk={} targetSdk={} sharedUserId={}",
694 value(&self.package),
695 opt(&self.version_code),
696 opt(&self.version_name),
697 opt_i(self.min_sdk),
698 opt_i(self.target_sdk),
699 opt(&self.shared_user_id)
700 )];
701 let a = &self.application;
702 out.push(format!(
703 "application class={} debuggable={} allowBackup={} testOnly={} usesCleartextTraffic={} cleartext={}({}) networkSecurityConfig={} permission={} fullBackupContent={} dataExtractionRules={}",
704 opt(&a.class),
705 a.debuggable,
706 a.allow_backup,
707 a.test_only,
708 a.uses_cleartext_traffic.map_or("-".to_string(), |b| b.to_string()),
709 a.cleartext_effective.0,
710 a.cleartext_effective.1,
711 opt(&a.network_security_config),
712 opt(&a.permission),
713 opt(&a.full_backup_content),
714 opt(&a.data_extraction_rules),
715 ));
716 out.extend(self.uses_permissions.iter().map(|p| format!("uses-permission {}", value(p))));
717 out.extend(
718 self.permissions.iter().map(|p| format!("permission {} protectionLevel={}", value(&p.name), value(&p.protection_level))),
719 );
720 for c in &self.components {
721 out.push(c.line());
722 let class = value(&c.class);
723 for f in &c.intent_filters {
724 out.push(format!("intent-filter {class} {}", f.line()));
725 }
726 for l in c.deep_links() {
727 out.push(format!("deeplink {class} {}", value(&l)));
728 }
729 for p in &c.provider_paths {
730 out.push(format!("provider-path {} {p}", c.authorities()));
731 }
732 }
733 out.extend(self.network_security.iter().map(|l| format!("nsc {l}")));
734 out.extend(self.warnings.iter().map(|w| format!("warning {w}")));
735 out
736 }
737
738 pub fn json(&self) -> Json {
740 let a = &self.application;
741 json!({
742 "package": self.package,
743 "version_code": self.version_code,
744 "version_name": self.version_name,
745 "min_sdk": self.min_sdk,
746 "target_sdk": self.target_sdk,
747 "shared_user_id": self.shared_user_id,
748 "application": {
749 "class": a.class,
750 "debuggable": a.debuggable,
751 "allow_backup": a.allow_backup,
752 "test_only": a.test_only,
753 "uses_cleartext_traffic": a.uses_cleartext_traffic,
754 "cleartext": a.cleartext_effective.0,
755 "cleartext_source": a.cleartext_effective.1,
756 "network_security_config": a.network_security_config,
757 "permission": a.permission,
758 "full_backup_content": a.full_backup_content,
759 "data_extraction_rules": a.data_extraction_rules,
760 },
761 "uses_permissions": self.uses_permissions,
762 "permissions": self.permissions.iter().map(|p| json!({ "name": p.name, "protection_level": p.protection_level })).collect::<Vec<_>>(),
763 "components": self.components.iter().map(Component::json).collect::<Vec<_>>(),
764 "network_security": self.network_security,
765 "warnings": self.warnings,
766 })
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773
774 #[test]
775 fn class_names_and_levels() {
776 assert_eq!(class_name("com.a", ".Main"), "com.a.Main");
777 assert_eq!(class_name("com.a", "Main"), "com.a.Main");
778 assert_eq!(class_name("com.a", "org.b.Main"), "org.b.Main");
779 assert_eq!(protection_level_name(0), "normal");
780 assert_eq!(protection_level_name(0x12), "signature|privileged");
781 assert_eq!(protection_level_name(0x4000_0002), "signature|0x40000000");
782 }
783
784 #[test]
785 fn deep_link_patterns() {
786 let f = IntentFilter {
787 actions: vec!["android.intent.action.VIEW".into()],
788 categories: vec!["android.intent.category.BROWSABLE".into()],
789 data: vec![
790 DataSpec { scheme: Some("https".into()), ..Default::default() },
791 DataSpec { host: Some("a.com".into()), ..Default::default() },
792 DataSpec { path: Some(("pathPrefix".into(), "/x".into())), ..Default::default() },
793 DataSpec { scheme: Some("myapp".into()), ..Default::default() },
794 ],
795 };
796 assert_eq!(f.deep_links(), ["https://a.com/x*", "myapp://a.com/x*"]);
797 let none = IntentFilter { actions: vec!["android.intent.action.MAIN".into()], ..Default::default() };
798 assert!(none.deep_links().is_empty());
799 }
800}