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