1use std::fmt;
9
10use serde_json::Value;
11
12use crate::HapError;
13use crate::value::{self, Findings, Line, Note, Obj};
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct ModuleJson {
18 pub app: App,
19 pub module: Module,
20 pub notes: Vec<Note>,
22 pub unmodelled: Vec<String>,
25 raw: Value,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct ApiVersion(pub i64);
35
36impl ApiVersion {
37 pub fn level(self) -> i64 {
39 self.0 % 1000
40 }
41}
42
43impl fmt::Display for ApiVersion {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 if self.level() == self.0 { write!(f, "{}", self.0) } else { write!(f, "{}({})", self.0, self.level()) }
47 }
48}
49
50#[derive(Debug, Clone, Default, PartialEq)]
52pub struct App {
53 pub bundle_name: Option<String>,
54 pub bundle_type: Option<String>,
56 pub version_code: Option<i64>,
57 pub version_name: Option<String>,
58 pub min_compatible_version_code: Option<i64>,
59 pub vendor: Option<String>,
60 pub debug: Option<bool>,
61 pub min_api_version: Option<ApiVersion>,
62 pub target_api_version: Option<ApiVersion>,
63 pub target_minor_api_version: Option<i64>,
64 pub target_patch_api_version: Option<i64>,
65 pub min_minor_api_version: Option<i64>,
66 pub min_patch_api_version: Option<i64>,
67 pub api_release_type: Option<String>,
68 pub compile_sdk_type: Option<String>,
69 pub compile_sdk_version: Option<String>,
70 pub build_mode: Option<String>,
72 pub label: Option<String>,
73 pub description: Option<String>,
74 pub environments: Vec<NameValue>,
75 pub multi_app_mode: Option<MultiAppMode>,
77 pub gwp_asan_enabled: Option<bool>,
79}
80
81#[derive(Debug, Clone, Default, PartialEq, Eq)]
83pub struct MultiAppMode {
84 pub kind: Option<String>,
86 pub max_count: Option<i64>,
87}
88
89#[derive(Debug, Clone, Default, PartialEq, Eq)]
91pub struct NameValue {
92 pub name: Option<String>,
93 pub value: Option<String>,
94}
95
96#[derive(Debug, Clone, Default, PartialEq, Eq)]
98pub struct Metadata {
99 pub name: Option<String>,
100 pub value: Option<String>,
101 pub resource: Option<String>,
102}
103
104#[derive(Debug, Clone, Default, PartialEq)]
106pub struct Module {
107 pub name: Option<String>,
108 pub kind: Option<String>,
110 pub package_name: Option<String>,
112 pub src_entry: Option<String>,
114 pub main_element: Option<String>,
115 pub description: Option<String>,
116 pub process: Option<String>,
117 pub device_types: Vec<String>,
118 pub virtual_machine: Option<String>,
119 pub compile_mode: Option<String>,
120 pub installation_free: Option<bool>,
121 pub delivery_with_install: Option<bool>,
122 pub compress_native_libs: Option<bool>,
123 pub isolation_mode: Option<String>,
124 pub pages: Option<String>,
126 pub router_map: Option<String>,
128 pub app_startup: Option<String>,
130 pub target_module_name: Option<String>,
131 pub target_priority: Option<i64>,
132 pub metadata: Vec<Metadata>,
133 pub dependencies: Vec<Dependency>,
134 pub proxy_data: Vec<ProxyData>,
135 pub query_schemes: Vec<String>,
137 pub abilities: Vec<Ability>,
138 pub extension_abilities: Vec<Ability>,
139 pub request_permissions: Vec<RequestPermission>,
140 pub define_permissions: Vec<DefinePermission>,
141 pub test_runner: Option<TestRunner>,
142 pub atomic_service: Option<AtomicService>,
143}
144
145#[derive(Debug, Clone, Default, PartialEq, Eq)]
147pub struct Dependency {
148 pub bundle_name: Option<String>,
149 pub module_name: Option<String>,
150 pub version_code: Option<i64>,
151}
152
153#[derive(Debug, Clone, Default, PartialEq, Eq)]
155pub struct ProxyData {
156 pub uri: Option<String>,
157 pub required_read_permission: Option<String>,
158 pub required_write_permission: Option<String>,
159 pub metadata: Option<Metadata>,
160}
161
162#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
164pub enum AbilityKind {
165 #[default]
167 Ability,
168 Extension,
170}
171
172impl AbilityKind {
173 pub fn as_str(self) -> &'static str {
175 match self {
176 AbilityKind::Ability => "ability",
177 AbilityKind::Extension => "extension",
178 }
179 }
180}
181
182#[derive(Debug, Clone, Default, PartialEq)]
184pub struct Ability {
185 pub kind: AbilityKind,
186 pub name: Option<String>,
187 pub src_entry: Option<String>,
190 pub extension_type: Option<String>,
192 pub exported: Option<bool>,
194 pub permissions: Vec<String>,
196 pub skills: Vec<Skill>,
197 pub launch_type: Option<String>,
198 pub process: Option<String>,
199 pub label: Option<String>,
200 pub description: Option<String>,
201 pub background_modes: Vec<String>,
202 pub continuable: Option<bool>,
203 pub metadata: Vec<Metadata>,
204 pub uri: Option<String>,
206 pub read_permission: Option<String>,
207 pub write_permission: Option<String>,
208 pub extension_process_mode: Option<String>,
209 pub data_group_ids: Vec<String>,
210 pub remove_mission_after_terminate: Option<bool>,
211 pub raw: Value,
213}
214
215impl Ability {
216 pub fn is_exported(&self) -> bool {
218 self.exported == Some(true)
219 }
220
221 pub fn deep_links(&self) -> Vec<String> {
224 let mut links: Vec<String> = Vec::new();
225 for link in self.skills.iter().flat_map(|s| &s.uris).flat_map(SkillUri::patterns) {
226 if !links.contains(&link) {
227 links.push(link);
228 }
229 }
230 links
231 }
232
233 fn display_name(&self) -> &str {
234 self.name.as_deref().unwrap_or("-")
235 }
236
237 fn line(&self) -> String {
238 let reason = if self.exported.is_some() { "explicit" } else { "default" };
239 let mut line = Line::new(self.kind.as_str()).word(self.display_name());
240 if self.kind == AbilityKind::Extension || self.extension_type.is_some() {
241 line = line.opt("type", self.extension_type.as_deref());
242 }
243 line = line
244 .raw("exported", format_args!("{} ({reason})", self.is_exported()))
245 .list("permissions", &self.permissions)
246 .raw("skills", self.skills.len())
247 .opt("srcEntry", self.src_entry.as_deref());
248 if self.uri.is_some() || self.read_permission.is_some() || self.write_permission.is_some() {
249 line = line
250 .opt("uri", self.uri.as_deref())
251 .opt("readPermission", self.read_permission.as_deref())
252 .opt("writePermission", self.write_permission.as_deref());
253 }
254 line.opt_present("launchType", self.launch_type.as_deref())
255 .opt_present("process", self.process.as_deref())
256 .opt_present("extensionProcessMode", self.extension_process_mode.as_deref())
257 .flag_present("continuable", self.continuable)
258 .list_present("backgroundModes", &self.background_modes)
259 .list_present("dataGroupIds", &self.data_group_ids)
260 .flag_present("removeMissionAfterTerminate", self.remove_mission_after_terminate)
261 .end()
262 }
263}
264
265#[derive(Debug, Clone, Default, PartialEq, Eq)]
267pub struct Skill {
268 pub actions: Vec<String>,
269 pub entities: Vec<String>,
270 pub uris: Vec<SkillUri>,
271 pub permissions: Vec<String>,
272 pub domain_verify: Option<bool>,
274}
275
276#[derive(Debug, Clone, Default, PartialEq, Eq)]
278pub struct SkillUri {
279 pub scheme: Option<String>,
280 pub host: Option<String>,
281 pub port: Option<String>,
282 pub path: Option<String>,
283 pub path_start_with: Option<String>,
284 pub path_regex: Option<String>,
285 pub mime_type: Option<String>,
287 pub utd: Option<String>,
289 pub max_file_supported: Option<i64>,
290 pub link_feature: Option<String>,
291}
292
293impl SkillUri {
294 pub fn patterns(&self) -> Vec<String> {
300 let Some(scheme) = &self.scheme else {
301 return Vec::new();
302 };
303 let paths: Vec<String> = [
304 self.path.as_ref().map(|p| p.trim_start_matches('/').to_owned()),
305 self.path_start_with.as_ref().map(|p| format!("{}*", p.trim_start_matches('/'))),
306 self.path_regex.as_ref().map(|p| format!("regex:{p}")),
307 ]
308 .into_iter()
309 .flatten()
310 .collect();
311 let authority = match (&self.host, &self.port) {
312 (Some(host), Some(port)) => format!("{host}:{port}"),
313 (Some(host), None) => host.clone(),
314 (None, Some(port)) => format!("*:{port}"),
315 (None, None) if paths.is_empty() => return vec![format!("{scheme}://*")],
316 (None, None) => "*".to_owned(),
317 };
318 if paths.is_empty() {
319 return vec![format!("{scheme}://{authority}")];
320 }
321 paths.iter().map(|path| format!("{scheme}://{authority}/{path}")).collect()
322 }
323
324 fn line(&self, owner: &str, skill: usize) -> String {
325 Line::new("uri")
326 .word(owner)
327 .raw("skill", skill)
328 .opt_present("scheme", self.scheme.as_deref())
329 .opt_present("host", self.host.as_deref())
330 .opt_present("port", self.port.as_deref())
331 .opt_present("path", self.path.as_deref())
332 .opt_present("pathStartWith", self.path_start_with.as_deref())
333 .opt_present("pathRegex", self.path_regex.as_deref())
334 .opt_present("type", self.mime_type.as_deref())
335 .opt_present("utd", self.utd.as_deref())
336 .opt_present("maxFileSupported", self.max_file_supported.map(|n| n.to_string()).as_deref())
337 .opt_present("linkFeature", self.link_feature.as_deref())
338 .end()
339 }
340}
341
342#[derive(Debug, Clone, Default, PartialEq, Eq)]
344pub struct RequestPermission {
345 pub name: Option<String>,
346 pub reason: Option<String>,
347 pub used_abilities: Vec<String>,
349 pub used_when: Option<String>,
351}
352
353#[derive(Debug, Clone, Default, PartialEq, Eq)]
355pub struct DefinePermission {
356 pub name: Option<String>,
357 pub grant_mode: Option<String>,
359 pub available_level: Option<String>,
361 pub provision_enable: Option<bool>,
362 pub distributed_scene_enable: Option<bool>,
363 pub label: Option<String>,
364 pub description: Option<String>,
365}
366
367#[derive(Debug, Clone, Default, PartialEq, Eq)]
369pub struct TestRunner {
370 pub name: Option<String>,
371 pub src_path: Option<String>,
372}
373
374#[derive(Debug, Clone, Default, PartialEq, Eq)]
376pub struct AtomicService {
377 pub preloads: Vec<String>,
379}
380
381const PRESENTATION_KEYS: &[&str] = &[
383 "icon",
384 "startWindow",
385 "startWindowIcon",
386 "startWindowBackground",
387 "orientation",
388 "supportWindowMode",
389 "maxWindowRatio",
390 "minWindowRatio",
391 "maxWindowWidth",
392 "minWindowWidth",
393 "maxWindowHeight",
394 "minWindowHeight",
395];
396
397impl ModuleJson {
398 pub fn parse(bytes: &[u8]) -> Result<Self, HapError> {
406 let map = value::parse_object(bytes)?;
407 let mut findings = Findings::default();
408 let mut root = Obj::root(&map, &mut findings);
409 let app = root.object("app", read_app).unwrap_or_default();
410 let module = root.object("module", read_module).unwrap_or_default();
411 root.finish();
412 Ok(ModuleJson { app, module, notes: findings.notes, unmodelled: findings.unmodelled, raw: Value::Object(map) })
413 }
414
415 pub fn raw(&self) -> &Value {
417 &self.raw
418 }
419
420 pub fn pretty(&self) -> String {
422 value::pretty(&self.raw)
423 }
424
425 pub fn components(&self) -> impl Iterator<Item = &Ability> {
427 self.module.abilities.iter().chain(&self.module.extension_abilities)
428 }
429
430 pub fn summary_lines(&self) -> Vec<String> {
442 let app = &self.app;
443 let module = &self.module;
444 let mut bundle = Line::new("bundle")
445 .word(app.bundle_name.as_deref().unwrap_or("-"))
446 .opt("bundleType", app.bundle_type.as_deref())
447 .num("versionCode", app.version_code)
448 .opt("versionName", app.version_name.as_deref())
449 .opt("vendor", app.vendor.as_deref())
450 .flag("debug", app.debug)
451 .num("minAPIVersion", app.min_api_version)
452 .num("targetAPIVersion", app.target_api_version)
453 .opt("apiReleaseType", app.api_release_type.as_deref())
454 .opt("compileSdkType", app.compile_sdk_type.as_deref())
455 .opt("compileSdkVersion", app.compile_sdk_version.as_deref())
456 .opt("buildMode", app.build_mode.as_deref());
457 if let Some(mode) = &app.multi_app_mode {
458 bundle = bundle.opt("multiAppMode", mode.kind.as_deref()).num("multiAppMaxCount", mode.max_count);
459 }
460 let mut out = vec![bundle.flag_present("GWPAsanEnabled", app.gwp_asan_enabled).end()];
461 out.extend(
462 app.environments.iter().map(|e| Line::new("app-env").opt("name", e.name.as_deref()).opt("value", e.value.as_deref()).end()),
463 );
464 out.push(
465 Line::new("module")
466 .word(module.name.as_deref().unwrap_or("-"))
467 .opt("type", module.kind.as_deref())
468 .opt("packageName", module.package_name.as_deref())
469 .opt("mainElement", module.main_element.as_deref())
470 .opt("srcEntry", module.src_entry.as_deref())
471 .list("deviceTypes", &module.device_types)
472 .opt("virtualMachine", module.virtual_machine.as_deref())
473 .opt("compileMode", module.compile_mode.as_deref())
474 .flag("installationFree", module.installation_free)
475 .flag("deliveryWithInstall", module.delivery_with_install)
476 .opt("process", module.process.as_deref())
477 .opt("pages", module.pages.as_deref())
478 .opt_present("routerMap", module.router_map.as_deref())
479 .opt_present("appStartup", module.app_startup.as_deref())
480 .flag_present("compressNativeLibs", module.compress_native_libs)
481 .opt_present("isolationMode", module.isolation_mode.as_deref())
482 .opt_present("targetModuleName", module.target_module_name.as_deref())
483 .opt_present("targetPriority", module.target_priority.map(|n| n.to_string()).as_deref())
484 .end(),
485 );
486 out.extend(module.metadata.iter().map(|m| metadata_line(Line::new("module-metadata"), m)));
487 out.extend(module.dependencies.iter().map(|d| {
488 Line::new("dependency")
489 .opt("bundleName", d.bundle_name.as_deref())
490 .opt("moduleName", d.module_name.as_deref())
491 .num("versionCode", d.version_code)
492 .end()
493 }));
494 out.extend(module.query_schemes.iter().map(|s| Line::new("query-scheme").word(s).end()));
495 out.extend(module.proxy_data.iter().map(|p| {
496 let line = Line::new("proxy-data")
497 .opt("uri", p.uri.as_deref())
498 .opt("requiredReadPermission", p.required_read_permission.as_deref())
499 .opt("requiredWritePermission", p.required_write_permission.as_deref());
500 match &p.metadata {
501 Some(m) => line.opt("metadataName", m.name.as_deref()).opt("metadataResource", m.resource.as_deref()).end(),
502 None => line.end(),
503 }
504 }));
505 out.extend(module.request_permissions.iter().map(|p| {
506 Line::new("request-permission")
507 .word(p.name.as_deref().unwrap_or("-"))
508 .opt("reason", p.reason.as_deref())
509 .opt("when", p.used_when.as_deref())
510 .list("abilities", &p.used_abilities)
511 .end()
512 }));
513 out.extend(module.define_permissions.iter().map(|p| {
514 Line::new("define-permission")
515 .word(p.name.as_deref().unwrap_or("-"))
516 .opt("grantMode", p.grant_mode.as_deref())
517 .opt("availableLevel", p.available_level.as_deref())
518 .flag("provisionEnable", p.provision_enable)
519 .flag("distributedSceneEnable", p.distributed_scene_enable)
520 .end()
521 }));
522 if let Some(t) = &module.test_runner {
523 out.push(Line::new("test-runner").opt("name", t.name.as_deref()).opt("srcPath", t.src_path.as_deref()).end());
524 }
525 if let Some(a) = &module.atomic_service {
526 out.push(Line::new("atomic-service").list("preloads", &a.preloads).end());
527 }
528 for component in self.components() {
529 let name = component.display_name();
530 out.push(component.line());
531 for (i, skill) in component.skills.iter().enumerate() {
532 let number = i + 1;
533 out.push(
534 Line::new("skill")
535 .word(name)
536 .raw("skill", number)
537 .list("actions", &skill.actions)
538 .list("entities", &skill.entities)
539 .raw("uris", skill.uris.len())
540 .list("permissions", &skill.permissions)
541 .flag_present("domainVerify", skill.domain_verify)
542 .end(),
543 );
544 out.extend(skill.uris.iter().map(|u| u.line(name, number)));
545 }
546 out.extend(component.deep_links().iter().map(|l| Line::new("deeplink").word(name).word(l).end()));
547 out.extend(component.metadata.iter().map(|m| metadata_line(Line::new("metadata").word(name), m)));
548 }
549 out.extend(self.unmodelled.iter().map(|path| Line::new("unmodelled").word(path).end()));
550 out.extend(self.notes.iter().map(|note| format!("note {note}")));
551 out
552 }
553
554 pub fn summary(&self) -> String {
556 crate::join_lines(&self.summary_lines())
557 }
558}
559
560fn metadata_line(line: Line, m: &Metadata) -> String {
561 line.opt("name", m.name.as_deref()).opt_present("value", m.value.as_deref()).opt_present("resource", m.resource.as_deref()).end()
562}
563
564fn read_app(o: &mut Obj<'_, '_>) -> App {
565 o.ignore(&["icon"]);
566 App {
567 bundle_name: o.str("bundleName"),
568 bundle_type: o.str("bundleType"),
569 version_code: o.int("versionCode"),
570 version_name: o.str("versionName"),
571 min_compatible_version_code: o.int("minCompatibleVersionCode"),
572 vendor: o.str("vendor"),
573 debug: o.bool("debug"),
574 min_api_version: o.int("minAPIVersion").map(ApiVersion),
575 target_api_version: o.int("targetAPIVersion").map(ApiVersion),
576 target_minor_api_version: o.int("targetMinorAPIVersion"),
577 target_patch_api_version: o.int("targetPatchAPIVersion"),
578 min_minor_api_version: o.int("minMinorAPIVersion"),
579 min_patch_api_version: o.int("minPatchAPIVersion"),
580 api_release_type: o.str("apiReleaseType"),
581 compile_sdk_type: o.str("compileSdkType"),
582 compile_sdk_version: o.text("compileSdkVersion"),
583 build_mode: o.str("buildMode"),
584 label: o.str("label"),
585 description: o.str("description"),
586 environments: o.objects("appEnvironments", |e| NameValue { name: e.str("name"), value: e.text("value") }),
587 multi_app_mode: o.object("multiAppMode", |m| MultiAppMode { kind: m.str("multiAppModeType"), max_count: m.int("maxCount") }),
588 gwp_asan_enabled: o.bool("GWPAsanEnabled"),
589 }
590}
591
592fn read_metadata(o: &mut Obj<'_, '_>) -> Metadata {
593 Metadata { name: o.str("name"), value: o.text("value"), resource: o.str("resource") }
594}
595
596fn read_module(o: &mut Obj<'_, '_>) -> Module {
597 Module {
598 name: o.str("name"),
599 kind: o.str("type"),
600 package_name: o.str("packageName"),
601 src_entry: o.str("srcEntry"),
602 main_element: o.str("mainElement"),
603 description: o.str("description"),
604 process: o.str("process"),
605 device_types: o.strings("deviceTypes"),
606 virtual_machine: o.str("virtualMachine"),
607 compile_mode: o.str("compileMode"),
608 installation_free: o.bool("installationFree"),
609 delivery_with_install: o.bool("deliveryWithInstall"),
610 compress_native_libs: o.bool("compressNativeLibs"),
611 isolation_mode: o.str("isolationMode"),
612 pages: o.str("pages"),
613 router_map: o.str("routerMap"),
614 app_startup: o.str("appStartup"),
615 target_module_name: o.str("targetModuleName"),
616 target_priority: o.int("targetPriority"),
617 metadata: o.objects("metadata", read_metadata),
618 dependencies: o.objects("dependencies", |d| Dependency {
619 bundle_name: d.str("bundleName"),
620 module_name: d.str("moduleName"),
621 version_code: d.int("versionCode"),
622 }),
623 proxy_data: o.objects("proxyData", |p| ProxyData {
624 uri: p.str("uri"),
625 required_read_permission: p.str("requiredReadPermission"),
626 required_write_permission: p.str("requiredWritePermission"),
627 metadata: p.object("metadata", read_metadata),
628 }),
629 query_schemes: o.strings("querySchemes"),
630 abilities: o.objects("abilities", |a| read_ability(a, AbilityKind::Ability)),
631 extension_abilities: o.objects("extensionAbilities", |a| read_ability(a, AbilityKind::Extension)),
632 request_permissions: o.objects("requestPermissions", |p| {
633 let scene = p.object("usedScene", |s| (s.strings("abilities"), s.str("when")));
634 let (used_abilities, used_when) = scene.unwrap_or_default();
635 RequestPermission { name: p.str("name"), reason: p.str("reason"), used_abilities, used_when }
636 }),
637 define_permissions: o.objects("definePermissions", |p| DefinePermission {
638 name: p.str("name"),
639 grant_mode: p.str("grantMode"),
640 available_level: p.str("availableLevel"),
641 provision_enable: p.bool("provisionEnable"),
642 distributed_scene_enable: p.bool("distributedSceneEnable"),
643 label: p.str("label"),
644 description: p.str("description"),
645 }),
646 test_runner: o.object("testRunner", |t| TestRunner { name: t.str("name"), src_path: t.str("srcPath") }),
647 atomic_service: o.object("atomicService", |a| AtomicService {
648 preloads: a.objects("preloads", |p| p.str("moduleName")).into_iter().flatten().collect(),
649 }),
650 }
651}
652
653fn read_ability(o: &mut Obj<'_, '_>, kind: AbilityKind) -> Ability {
654 o.ignore(PRESENTATION_KEYS);
655 Ability {
656 kind,
657 name: o.str("name"),
658 src_entry: o.str("srcEntry"),
659 extension_type: o.str("type"),
660 exported: o.bool("exported"),
661 permissions: o.strings("permissions"),
662 skills: o.objects("skills", read_skill),
663 launch_type: o.str("launchType"),
664 process: o.str("process"),
665 label: o.str("label"),
666 description: o.str("description"),
667 background_modes: o.strings("backgroundModes"),
668 continuable: o.bool("continuable"),
669 metadata: o.objects("metadata", read_metadata),
670 uri: o.str("uri"),
671 read_permission: o.str("readPermission"),
672 write_permission: o.str("writePermission"),
673 extension_process_mode: o.str("extensionProcessMode"),
674 data_group_ids: o.strings("dataGroupIds"),
675 remove_mission_after_terminate: o.bool("removeMissionAfterTerminate"),
676 raw: o.to_value(),
677 }
678}
679
680fn read_skill(o: &mut Obj<'_, '_>) -> Skill {
681 Skill {
682 actions: o.strings("actions"),
683 entities: o.strings("entities"),
684 uris: o.objects("uris", |u| SkillUri {
685 scheme: u.str("scheme"),
686 host: u.str("host"),
687 port: u.text("port"),
688 path: u.str("path"),
689 path_start_with: u.str("pathStartWith"),
690 path_regex: u.str("pathRegex"),
691 mime_type: u.str("type"),
692 utd: u.str("utd"),
693 max_file_supported: u.int("maxFileSupported"),
694 link_feature: u.str("linkFeature"),
695 }),
696 permissions: o.strings("permissions"),
697 domain_verify: o.bool("domainVerify"),
698 }
699}