Skip to main content

asc_hap/
module.rs

1//! `module.json`: the compiled module descriptor at the root of a HAP/HSP.
2//!
3//! This is the packed form, not the developer's `module.json5`: the `app`
4//! object is merged in from `app.json5`, resource references are spelled
5//! `$string:name` with a numeric sibling (`labelId`), and build facts such
6//! as `virtualMachine` and `compileSdkVersion` are added.
7
8use std::fmt;
9
10use serde_json::Value;
11
12use crate::HapError;
13use crate::value::{self, Findings, Line, Note, Obj};
14
15/// A parsed `module.json`.
16#[derive(Debug, Clone, PartialEq)]
17#[non_exhaustive]
18pub struct ModuleJson {
19    /// The `app` object.
20    pub app: App,
21    /// The `module` object.
22    pub module: Module,
23    /// Fields that were present but wrongly typed, and so skipped.
24    pub notes: Vec<Note>,
25    /// JSON paths of fields this model has no place for; read them from
26    /// [`ModuleJson::raw`].
27    pub unmodelled: Vec<String>,
28    raw: Value,
29}
30
31/// `minAPIVersion` / `targetAPIVersion` as written.
32///
33/// Old packages write the API level itself (`9`); newer ones write an
34/// eight-digit number whose last three digits are the level
35/// (`50001013` is level 13).
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct ApiVersion(pub i64);
38
39impl ApiVersion {
40    /// The API level, as `pack.info` states it in `apiVersion.compatible`.
41    pub fn level(self) -> i64 {
42        self.0 % 1000
43    }
44}
45
46/// `50001013(13)`, or just `9` when the value is the level.
47impl fmt::Display for ApiVersion {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        if self.level() == self.0 { write!(f, "{}", self.0) } else { write!(f, "{}({})", self.0, self.level()) }
50    }
51}
52
53/// The `app` object: bundle-wide facts.
54#[derive(Debug, Clone, Default, PartialEq)]
55#[non_exhaustive]
56pub struct App {
57    /// `bundleName`: the reverse-domain bundle identifier.
58    pub bundle_name: Option<String>,
59    /// `app`, `atomicService`, `shared`, `appService`…
60    pub bundle_type: Option<String>,
61    /// `versionCode`: the integer version compared on upgrade.
62    pub version_code: Option<i64>,
63    /// `versionName`: the version shown to users.
64    pub version_name: Option<String>,
65    /// `minCompatibleVersionCode`: the oldest version this one can replace across devices.
66    pub min_compatible_version_code: Option<i64>,
67    /// `vendor`.
68    pub vendor: Option<String>,
69    /// `debug`: whether the bundle was built debuggable.
70    pub debug: Option<bool>,
71    /// `minAPIVersion`: the lowest API level the bundle runs on.
72    pub min_api_version: Option<ApiVersion>,
73    /// `targetAPIVersion`: the API level the bundle was built against.
74    pub target_api_version: Option<ApiVersion>,
75    /// `targetMinorAPIVersion`.
76    pub target_minor_api_version: Option<i64>,
77    /// `targetPatchAPIVersion`.
78    pub target_patch_api_version: Option<i64>,
79    /// `minMinorAPIVersion`.
80    pub min_minor_api_version: Option<i64>,
81    /// `minPatchAPIVersion`.
82    pub min_patch_api_version: Option<i64>,
83    /// `apiReleaseType`: the SDK release channel (`Release`, `Beta1`…).
84    pub api_release_type: Option<String>,
85    /// `compileSdkType`: `HarmonyOS` or `OpenHarmony`.
86    pub compile_sdk_type: Option<String>,
87    /// `compileSdkVersion`: the SDK version string the bundle was compiled with.
88    pub compile_sdk_version: Option<String>,
89    /// `debug` or `release`.
90    pub build_mode: Option<String>,
91    /// `label`: usually a `$string:` resource reference.
92    pub label: Option<String>,
93    /// `description`: usually a `$string:` resource reference.
94    pub description: Option<String>,
95    /// `appEnvironments`: environment variables set for the app process.
96    pub environments: Vec<NameValue>,
97    /// `multiAppMode`: whether the bundle may be installed several times.
98    pub multi_app_mode: Option<MultiAppMode>,
99    /// `GWPAsanEnabled`: sampled heap checking in release builds.
100    pub gwp_asan_enabled: Option<bool>,
101}
102
103/// `multiAppMode` of the `app` object.
104#[derive(Debug, Clone, Default, PartialEq, Eq)]
105#[non_exhaustive]
106pub struct MultiAppMode {
107    /// `multiAppModeType`: `appClone` or `multiInstance`.
108    pub kind: Option<String>,
109    /// `maxCount`: how many clones or instances may exist.
110    pub max_count: Option<i64>,
111}
112
113/// A `name` / `value` pair (`appEnvironments`).
114#[derive(Debug, Clone, Default, PartialEq, Eq)]
115#[non_exhaustive]
116pub struct NameValue {
117    /// `name`.
118    pub name: Option<String>,
119    /// `value`, as text even when written as a number.
120    pub value: Option<String>,
121}
122
123/// A `metadata` entry: a literal `value`, a `$profile:` `resource`, or both.
124#[derive(Debug, Clone, Default, PartialEq, Eq)]
125#[non_exhaustive]
126pub struct Metadata {
127    /// `name`: the metadata key.
128    pub name: Option<String>,
129    /// `value`: a literal value, as text.
130    pub value: Option<String>,
131    /// `resource`: a `$profile:` reference to a JSON file under `resources/base/profile`.
132    pub resource: Option<String>,
133}
134
135/// The `module` object.
136#[derive(Debug, Clone, Default, PartialEq)]
137#[non_exhaustive]
138pub struct Module {
139    /// `name`: the module name, unique within the bundle.
140    pub name: Option<String>,
141    /// `entry`, `feature`, `har` or `shared`.
142    pub kind: Option<String>,
143    /// The module's package name: its key in `pkgContextInfo.json`.
144    pub package_name: Option<String>,
145    /// Source of the module's `AbilityStage`.
146    pub src_entry: Option<String>,
147    /// `mainElement`: the ability or extension that starts first.
148    pub main_element: Option<String>,
149    /// `description`: usually a `$string:` resource reference.
150    pub description: Option<String>,
151    /// `process`: the process name the module runs in.
152    pub process: Option<String>,
153    /// `deviceTypes`: `phone`, `tablet`, `2in1`, `wearable`…
154    pub device_types: Vec<String>,
155    /// `virtualMachine`: the runtime the code targets, e.g. `ark12.0.2.0`.
156    pub virtual_machine: Option<String>,
157    /// `compileMode`: `esmodule` or `jsbundle`.
158    pub compile_mode: Option<String>,
159    /// `installationFree`: whether the module runs without installation (atomic service).
160    pub installation_free: Option<bool>,
161    /// `deliveryWithInstall`: whether the module is installed with the app.
162    pub delivery_with_install: Option<bool>,
163    /// `compressNativeLibs`: whether the `.so` files are stored compressed.
164    pub compress_native_libs: Option<bool>,
165    /// `isolationMode`: whether the module runs in an isolated process.
166    pub isolation_mode: Option<String>,
167    /// `$profile:` reference to the page list.
168    pub pages: Option<String>,
169    /// `$profile:` reference to the navigation route table.
170    pub router_map: Option<String>,
171    /// `$profile:` reference to the startup task configuration.
172    pub app_startup: Option<String>,
173    /// `targetModuleName`: the module an overlay module applies to.
174    pub target_module_name: Option<String>,
175    /// `targetPriority`: the overlay's priority over its target.
176    pub target_priority: Option<i64>,
177    /// `metadata`: custom key/value entries of the module.
178    pub metadata: Vec<Metadata>,
179    /// `dependencies`: shared packages the module links against.
180    pub dependencies: Vec<Dependency>,
181    /// `proxyData`: data the module shares through the data-share proxy.
182    pub proxy_data: Vec<ProxyData>,
183    /// URL schemes the module may probe with `canOpenLink`.
184    pub query_schemes: Vec<String>,
185    /// `abilities`: the module's UIAbilities.
186    pub abilities: Vec<Ability>,
187    /// `extensionAbilities`: the module's extension abilities.
188    pub extension_abilities: Vec<Ability>,
189    /// `requestPermissions`: permissions the module asks for.
190    pub request_permissions: Vec<RequestPermission>,
191    /// `definePermissions`: permissions the module declares for others.
192    pub define_permissions: Vec<DefinePermission>,
193    /// `testRunner`: the test runner of a test module.
194    pub test_runner: Option<TestRunner>,
195    /// `atomicService`: settings of an atomic-service module.
196    pub atomic_service: Option<AtomicService>,
197}
198
199/// A shared package (HSP) the module depends on.
200#[derive(Debug, Clone, Default, PartialEq, Eq)]
201#[non_exhaustive]
202pub struct Dependency {
203    /// `bundleName` of the shared package; `None` for one in the same bundle.
204    pub bundle_name: Option<String>,
205    /// `moduleName` of the shared package.
206    pub module_name: Option<String>,
207    /// `versionCode` the dependency requires.
208    pub version_code: Option<i64>,
209}
210
211/// A `proxyData` entry: data shared through the data-share proxy.
212#[derive(Debug, Clone, Default, PartialEq, Eq)]
213#[non_exhaustive]
214pub struct ProxyData {
215    /// `uri`: the `datashareproxy://` URI the data is published under.
216    pub uri: Option<String>,
217    /// `requiredReadPermission`: permission a reader must hold.
218    pub required_read_permission: Option<String>,
219    /// `requiredWritePermission`: permission a writer must hold.
220    pub required_write_permission: Option<String>,
221    /// `metadata`: the entry's single metadata object.
222    pub metadata: Option<Metadata>,
223}
224
225/// Which array of the module an [`Ability`] comes from.
226#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
227#[non_exhaustive]
228pub enum AbilityKind {
229    /// `abilities`: a UIAbility.
230    #[default]
231    Ability,
232    /// `extensionAbilities`: form, service, data share, backup…
233    Extension,
234}
235
236impl AbilityKind {
237    /// The record word of the summary line: `ability` or `extension`.
238    pub fn as_str(self) -> &'static str {
239        match self {
240            AbilityKind::Ability => "ability",
241            AbilityKind::Extension => "extension",
242        }
243    }
244}
245
246/// An entry of `abilities` or `extensionAbilities`.
247#[derive(Debug, Clone, Default, PartialEq)]
248#[non_exhaustive]
249pub struct Ability {
250    /// Which array the entry comes from.
251    pub kind: AbilityKind,
252    /// `name`: the class name of the ability.
253    pub name: Option<String>,
254    /// Source file relative to the module (`./ets/entryability/EntryAbility.ets`);
255    /// see [`ModuleJson::record_names`].
256    pub src_entry: Option<String>,
257    /// `type` of an extension ability (`form`, `service`, `dataShare`…).
258    pub extension_type: Option<String>,
259    /// As written; `None` when the field is absent.
260    pub exported: Option<bool>,
261    /// Permissions a caller must hold.
262    pub permissions: Vec<String>,
263    /// `skills`: the implicit wants the ability accepts.
264    pub skills: Vec<Skill>,
265    /// `launchType`: `singleton`, `multiton`, `specified` or `standard`.
266    pub launch_type: Option<String>,
267    /// `process`: the process name the ability runs in.
268    pub process: Option<String>,
269    /// `label`: usually a `$string:` resource reference.
270    pub label: Option<String>,
271    /// `description`: usually a `$string:` resource reference.
272    pub description: Option<String>,
273    /// `backgroundModes`: long-running task types the ability may start.
274    pub background_modes: Vec<String>,
275    /// `continuable`: whether the ability may migrate to another device.
276    pub continuable: Option<bool>,
277    /// `metadata`: custom key/value entries of the ability.
278    pub metadata: Vec<Metadata>,
279    /// Data URI served by a data extension.
280    pub uri: Option<String>,
281    /// `readPermission`: permission a caller needs to read from `uri`.
282    pub read_permission: Option<String>,
283    /// `writePermission`: permission a caller needs to write to `uri`.
284    pub write_permission: Option<String>,
285    /// `extensionProcessMode`: `instance`, `type` or `bundle`.
286    pub extension_process_mode: Option<String>,
287    /// `dataGroupIds`: data groups the extension may share files with.
288    pub data_group_ids: Vec<String>,
289    /// `removeMissionAfterTerminate`: whether the mission is dropped from recents when the ability ends.
290    pub remove_mission_after_terminate: Option<bool>,
291    /// The entry as written, for fields not modelled here.
292    pub raw: Value,
293}
294
295impl Ability {
296    /// The `exported` flag in effect: an absent field means not exported.
297    pub fn is_exported(&self) -> bool {
298        self.exported == Some(true)
299    }
300
301    /// URL patterns of every skill URI that names a scheme, in document
302    /// order without repeats.
303    pub fn deep_links(&self) -> Vec<String> {
304        let mut links: Vec<String> = Vec::new();
305        for link in self.skills.iter().flat_map(|s| &s.uris).flat_map(SkillUri::patterns) {
306            if !links.contains(&link) {
307                links.push(link);
308            }
309        }
310        links
311    }
312
313    fn display_name(&self) -> &str {
314        self.name.as_deref().unwrap_or("-")
315    }
316
317    fn line(&self) -> String {
318        let reason = if self.exported.is_some() { "explicit" } else { "default" };
319        let mut line = Line::new(self.kind.as_str()).word(self.display_name());
320        if self.kind == AbilityKind::Extension || self.extension_type.is_some() {
321            line = line.opt("type", self.extension_type.as_deref());
322        }
323        line = line
324            .raw("exported", format_args!("{} ({reason})", self.is_exported()))
325            .list("permissions", &self.permissions)
326            .raw("skills", self.skills.len())
327            .opt("srcEntry", self.src_entry.as_deref());
328        if self.uri.is_some() || self.read_permission.is_some() || self.write_permission.is_some() {
329            line = line
330                .opt("uri", self.uri.as_deref())
331                .opt("readPermission", self.read_permission.as_deref())
332                .opt("writePermission", self.write_permission.as_deref());
333        }
334        line.opt_present("launchType", self.launch_type.as_deref())
335            .opt_present("process", self.process.as_deref())
336            .opt_present("extensionProcessMode", self.extension_process_mode.as_deref())
337            .flag_present("continuable", self.continuable)
338            .list_present("backgroundModes", &self.background_modes)
339            .list_present("dataGroupIds", &self.data_group_ids)
340            .flag_present("removeMissionAfterTerminate", self.remove_mission_after_terminate)
341            .end()
342    }
343}
344
345/// One entry of `skills`: the implicit wants an ability accepts.
346#[derive(Debug, Clone, Default, PartialEq, Eq)]
347#[non_exhaustive]
348pub struct Skill {
349    /// `actions`: want actions the skill matches.
350    pub actions: Vec<String>,
351    /// `entities`: want entities the skill matches.
352    pub entities: Vec<String>,
353    /// `uris`: URI patterns the skill matches.
354    pub uris: Vec<SkillUri>,
355    /// `permissions`: permissions a caller must hold to use the skill.
356    pub permissions: Vec<String>,
357    /// App Linking: the `https` hosts are verified against the bundle.
358    pub domain_verify: Option<bool>,
359}
360
361/// One entry of a skill's `uris`.
362#[derive(Debug, Clone, Default, PartialEq, Eq)]
363#[non_exhaustive]
364pub struct SkillUri {
365    /// `scheme`, e.g. `https` or an app-private scheme.
366    pub scheme: Option<String>,
367    /// `host`.
368    pub host: Option<String>,
369    /// `port`, as text even when written as a number.
370    pub port: Option<String>,
371    /// `path`: an exact path.
372    pub path: Option<String>,
373    /// `pathStartWith`: a path prefix.
374    pub path_start_with: Option<String>,
375    /// `pathRegex`: a path regular expression.
376    pub path_regex: Option<String>,
377    /// MIME type of the data.
378    pub mime_type: Option<String>,
379    /// Uniform type descriptor of the data.
380    pub utd: Option<String>,
381    /// `maxFileSupported`: how many files of this type the ability accepts at once.
382    pub max_file_supported: Option<i64>,
383    /// `linkFeature`: the app-linking feature the URI serves (`Login`, `Pay`…).
384    pub link_feature: Option<String>,
385}
386
387impl SkillUri {
388    /// The URL patterns this entry spells, one per path form present:
389    /// `scheme://host:port/path`, `…/prefix*` for `pathStartWith` and
390    /// `…/regex:<re>` for `pathRegex`. A missing host prints as `*`, and
391    /// so does everything after a bare scheme (`myapp://*`). Empty
392    /// when the entry names no scheme.
393    pub fn patterns(&self) -> Vec<String> {
394        let Some(scheme) = &self.scheme else {
395            return Vec::new();
396        };
397        let paths: Vec<String> = [
398            self.path.as_ref().map(|p| p.trim_start_matches('/').to_owned()),
399            self.path_start_with.as_ref().map(|p| format!("{}*", p.trim_start_matches('/'))),
400            self.path_regex.as_ref().map(|p| format!("regex:{p}")),
401        ]
402        .into_iter()
403        .flatten()
404        .collect();
405        let authority = match (&self.host, &self.port) {
406            (Some(host), Some(port)) => format!("{host}:{port}"),
407            (Some(host), None) => host.clone(),
408            (None, Some(port)) => format!("*:{port}"),
409            (None, None) if paths.is_empty() => return vec![format!("{scheme}://*")],
410            (None, None) => "*".to_owned(),
411        };
412        if paths.is_empty() {
413            return vec![format!("{scheme}://{authority}")];
414        }
415        paths.iter().map(|path| format!("{scheme}://{authority}/{path}")).collect()
416    }
417
418    fn line(&self, owner: &str, skill: usize) -> String {
419        Line::new("uri")
420            .word(owner)
421            .raw("skill", skill)
422            .opt_present("scheme", self.scheme.as_deref())
423            .opt_present("host", self.host.as_deref())
424            .opt_present("port", self.port.as_deref())
425            .opt_present("path", self.path.as_deref())
426            .opt_present("pathStartWith", self.path_start_with.as_deref())
427            .opt_present("pathRegex", self.path_regex.as_deref())
428            .opt_present("type", self.mime_type.as_deref())
429            .opt_present("utd", self.utd.as_deref())
430            .opt_present("maxFileSupported", self.max_file_supported.map(|n| n.to_string()).as_deref())
431            .opt_present("linkFeature", self.link_feature.as_deref())
432            .end()
433    }
434}
435
436/// An entry of `requestPermissions`.
437#[derive(Debug, Clone, Default, PartialEq, Eq)]
438#[non_exhaustive]
439pub struct RequestPermission {
440    /// `name`: the permission requested.
441    pub name: Option<String>,
442    /// `reason`: usually a `$string:` resource reference shown to the user.
443    pub reason: Option<String>,
444    /// `usedScene.abilities`.
445    pub used_abilities: Vec<String>,
446    /// `usedScene.when`: `inuse` or `always`.
447    pub used_when: Option<String>,
448}
449
450/// An entry of `definePermissions`.
451#[derive(Debug, Clone, Default, PartialEq, Eq)]
452#[non_exhaustive]
453pub struct DefinePermission {
454    /// `name`: the permission declared.
455    pub name: Option<String>,
456    /// `system_grant` or `user_grant`.
457    pub grant_mode: Option<String>,
458    /// `normal`, `system_basic` or `system_core`.
459    pub available_level: Option<String>,
460    /// `provisionEnable`: whether the permission may be granted through the provision profile.
461    pub provision_enable: Option<bool>,
462    /// `distributedSceneEnable`: whether the permission applies in distributed scenarios.
463    pub distributed_scene_enable: Option<bool>,
464    /// `label`: usually a `$string:` resource reference.
465    pub label: Option<String>,
466    /// `description`: usually a `$string:` resource reference.
467    pub description: Option<String>,
468}
469
470/// `testRunner`.
471#[derive(Debug, Clone, Default, PartialEq, Eq)]
472#[non_exhaustive]
473pub struct TestRunner {
474    /// `name`: the test runner's name.
475    pub name: Option<String>,
476    /// `srcPath`: source of the test runner.
477    pub src_path: Option<String>,
478}
479
480/// `atomicService`.
481#[derive(Debug, Clone, Default, PartialEq, Eq)]
482#[non_exhaustive]
483pub struct AtomicService {
484    /// `preloads[].moduleName`.
485    pub preloads: Vec<String>,
486}
487
488/// Keys that only affect how a component looks.
489const PRESENTATION_KEYS: &[&str] = &[
490    "icon",
491    "startWindow",
492    "startWindowIcon",
493    "startWindowBackground",
494    "orientation",
495    "supportWindowMode",
496    "maxWindowRatio",
497    "minWindowRatio",
498    "maxWindowWidth",
499    "minWindowWidth",
500    "maxWindowHeight",
501    "minWindowHeight",
502];
503
504impl ModuleJson {
505    /// Read a `module.json`.
506    ///
507    /// # Errors
508    ///
509    /// Only when `bytes` is not JSON or its root is not an object. Missing,
510    /// unknown and wrongly typed fields never fail the document: see
511    /// [`ModuleJson::notes`] and [`ModuleJson::unmodelled`].
512    pub fn parse(bytes: &[u8]) -> Result<Self, HapError> {
513        let map = value::parse_object(bytes)?;
514        let mut findings = Findings::default();
515        let mut root = Obj::root(&map, &mut findings);
516        let app = root.object("app", read_app).unwrap_or_default();
517        let module = root.object("module", read_module).unwrap_or_default();
518        root.finish();
519        Ok(ModuleJson { app, module, notes: findings.notes, unmodelled: findings.unmodelled, raw: Value::Object(map) })
520    }
521
522    /// The whole document as parsed.
523    pub fn raw(&self) -> &Value {
524        &self.raw
525    }
526
527    /// The document pretty-printed, for `getmanifest`.
528    pub fn pretty(&self) -> String {
529        value::pretty(&self.raw)
530    }
531
532    /// Abilities, then extension abilities, in document order.
533    pub fn components(&self) -> impl Iterator<Item = &Ability> {
534        self.module.abilities.iter().chain(&self.module.extension_abilities)
535    }
536
537    /// One fact per line, the first word naming the record: `bundle`,
538    /// `app-env`, `module`, `module-metadata`, `dependency`, `query-scheme`,
539    /// `proxy-data`, `request-permission`, `define-permission`,
540    /// `test-runner`, `atomic-service`, then per component `ability` or
541    /// `extension` followed by its `skill`, `uri`, `deeplink` and
542    /// `metadata` lines (each carrying the component name), and finally
543    /// `unmodelled` and `note`.
544    ///
545    /// Absent values print as `-`; values with whitespace print as JSON
546    /// string literals. `exported=` is always printed, with `(explicit)`
547    /// or `(default)` telling whether the document said so.
548    pub fn summary_lines(&self) -> Vec<String> {
549        let app = &self.app;
550        let module = &self.module;
551        let mut bundle = Line::new("bundle")
552            .word(app.bundle_name.as_deref().unwrap_or("-"))
553            .opt("bundleType", app.bundle_type.as_deref())
554            .num("versionCode", app.version_code)
555            .opt("versionName", app.version_name.as_deref())
556            .opt("vendor", app.vendor.as_deref())
557            .flag("debug", app.debug)
558            .num("minAPIVersion", app.min_api_version)
559            .num("targetAPIVersion", app.target_api_version)
560            .opt("apiReleaseType", app.api_release_type.as_deref())
561            .opt("compileSdkType", app.compile_sdk_type.as_deref())
562            .opt("compileSdkVersion", app.compile_sdk_version.as_deref())
563            .opt("buildMode", app.build_mode.as_deref());
564        if let Some(mode) = &app.multi_app_mode {
565            bundle = bundle.opt("multiAppMode", mode.kind.as_deref()).num("multiAppMaxCount", mode.max_count);
566        }
567        let mut out = vec![bundle.flag_present("GWPAsanEnabled", app.gwp_asan_enabled).end()];
568        out.extend(
569            app.environments.iter().map(|e| Line::new("app-env").opt("name", e.name.as_deref()).opt("value", e.value.as_deref()).end()),
570        );
571        out.push(
572            Line::new("module")
573                .word(module.name.as_deref().unwrap_or("-"))
574                .opt("type", module.kind.as_deref())
575                .opt("packageName", module.package_name.as_deref())
576                .opt("mainElement", module.main_element.as_deref())
577                .opt("srcEntry", module.src_entry.as_deref())
578                .list("deviceTypes", &module.device_types)
579                .opt("virtualMachine", module.virtual_machine.as_deref())
580                .opt("compileMode", module.compile_mode.as_deref())
581                .flag("installationFree", module.installation_free)
582                .flag("deliveryWithInstall", module.delivery_with_install)
583                .opt("process", module.process.as_deref())
584                .opt("pages", module.pages.as_deref())
585                .opt_present("routerMap", module.router_map.as_deref())
586                .opt_present("appStartup", module.app_startup.as_deref())
587                .flag_present("compressNativeLibs", module.compress_native_libs)
588                .opt_present("isolationMode", module.isolation_mode.as_deref())
589                .opt_present("targetModuleName", module.target_module_name.as_deref())
590                .opt_present("targetPriority", module.target_priority.map(|n| n.to_string()).as_deref())
591                .end(),
592        );
593        out.extend(module.metadata.iter().map(|m| metadata_line(Line::new("module-metadata"), m)));
594        out.extend(module.dependencies.iter().map(|d| {
595            Line::new("dependency")
596                .opt("bundleName", d.bundle_name.as_deref())
597                .opt("moduleName", d.module_name.as_deref())
598                .num("versionCode", d.version_code)
599                .end()
600        }));
601        out.extend(module.query_schemes.iter().map(|s| Line::new("query-scheme").word(s).end()));
602        out.extend(module.proxy_data.iter().map(|p| {
603            let line = Line::new("proxy-data")
604                .opt("uri", p.uri.as_deref())
605                .opt("requiredReadPermission", p.required_read_permission.as_deref())
606                .opt("requiredWritePermission", p.required_write_permission.as_deref());
607            match &p.metadata {
608                Some(m) => line.opt("metadataName", m.name.as_deref()).opt("metadataResource", m.resource.as_deref()).end(),
609                None => line.end(),
610            }
611        }));
612        out.extend(module.request_permissions.iter().map(|p| {
613            Line::new("request-permission")
614                .word(p.name.as_deref().unwrap_or("-"))
615                .opt("reason", p.reason.as_deref())
616                .opt("when", p.used_when.as_deref())
617                .list("abilities", &p.used_abilities)
618                .end()
619        }));
620        out.extend(module.define_permissions.iter().map(|p| {
621            Line::new("define-permission")
622                .word(p.name.as_deref().unwrap_or("-"))
623                .opt("grantMode", p.grant_mode.as_deref())
624                .opt("availableLevel", p.available_level.as_deref())
625                .flag("provisionEnable", p.provision_enable)
626                .flag("distributedSceneEnable", p.distributed_scene_enable)
627                .end()
628        }));
629        if let Some(t) = &module.test_runner {
630            out.push(Line::new("test-runner").opt("name", t.name.as_deref()).opt("srcPath", t.src_path.as_deref()).end());
631        }
632        if let Some(a) = &module.atomic_service {
633            out.push(Line::new("atomic-service").list("preloads", &a.preloads).end());
634        }
635        for component in self.components() {
636            let name = component.display_name();
637            out.push(component.line());
638            for (i, skill) in component.skills.iter().enumerate() {
639                let number = i + 1;
640                out.push(
641                    Line::new("skill")
642                        .word(name)
643                        .raw("skill", number)
644                        .list("actions", &skill.actions)
645                        .list("entities", &skill.entities)
646                        .raw("uris", skill.uris.len())
647                        .list("permissions", &skill.permissions)
648                        .flag_present("domainVerify", skill.domain_verify)
649                        .end(),
650                );
651                out.extend(skill.uris.iter().map(|u| u.line(name, number)));
652            }
653            out.extend(component.deep_links().iter().map(|l| Line::new("deeplink").word(name).word(l).end()));
654            out.extend(component.metadata.iter().map(|m| metadata_line(Line::new("metadata").word(name), m)));
655        }
656        out.extend(self.unmodelled.iter().map(|path| Line::new("unmodelled").word(path).end()));
657        out.extend(self.notes.iter().map(|note| format!("note {note}")));
658        out
659    }
660
661    /// [`ModuleJson::summary_lines`] joined, each line newline-terminated.
662    pub fn summary(&self) -> String {
663        crate::join_lines(&self.summary_lines())
664    }
665}
666
667fn metadata_line(line: Line, m: &Metadata) -> String {
668    line.opt("name", m.name.as_deref()).opt_present("value", m.value.as_deref()).opt_present("resource", m.resource.as_deref()).end()
669}
670
671fn read_app(o: &mut Obj<'_, '_>) -> App {
672    o.ignore(&["icon"]);
673    App {
674        bundle_name: o.str("bundleName"),
675        bundle_type: o.str("bundleType"),
676        version_code: o.int("versionCode"),
677        version_name: o.str("versionName"),
678        min_compatible_version_code: o.int("minCompatibleVersionCode"),
679        vendor: o.str("vendor"),
680        debug: o.bool("debug"),
681        min_api_version: o.int("minAPIVersion").map(ApiVersion),
682        target_api_version: o.int("targetAPIVersion").map(ApiVersion),
683        target_minor_api_version: o.int("targetMinorAPIVersion"),
684        target_patch_api_version: o.int("targetPatchAPIVersion"),
685        min_minor_api_version: o.int("minMinorAPIVersion"),
686        min_patch_api_version: o.int("minPatchAPIVersion"),
687        api_release_type: o.str("apiReleaseType"),
688        compile_sdk_type: o.str("compileSdkType"),
689        compile_sdk_version: o.text("compileSdkVersion"),
690        build_mode: o.str("buildMode"),
691        label: o.str("label"),
692        description: o.str("description"),
693        environments: o.objects("appEnvironments", |e| NameValue { name: e.str("name"), value: e.text("value") }),
694        multi_app_mode: o.object("multiAppMode", |m| MultiAppMode { kind: m.str("multiAppModeType"), max_count: m.int("maxCount") }),
695        gwp_asan_enabled: o.bool("GWPAsanEnabled"),
696    }
697}
698
699fn read_metadata(o: &mut Obj<'_, '_>) -> Metadata {
700    Metadata { name: o.str("name"), value: o.text("value"), resource: o.str("resource") }
701}
702
703fn read_module(o: &mut Obj<'_, '_>) -> Module {
704    Module {
705        name: o.str("name"),
706        kind: o.str("type"),
707        package_name: o.str("packageName"),
708        src_entry: o.str("srcEntry"),
709        main_element: o.str("mainElement"),
710        description: o.str("description"),
711        process: o.str("process"),
712        device_types: o.strings("deviceTypes"),
713        virtual_machine: o.str("virtualMachine"),
714        compile_mode: o.str("compileMode"),
715        installation_free: o.bool("installationFree"),
716        delivery_with_install: o.bool("deliveryWithInstall"),
717        compress_native_libs: o.bool("compressNativeLibs"),
718        isolation_mode: o.str("isolationMode"),
719        pages: o.str("pages"),
720        router_map: o.str("routerMap"),
721        app_startup: o.str("appStartup"),
722        target_module_name: o.str("targetModuleName"),
723        target_priority: o.int("targetPriority"),
724        metadata: o.objects("metadata", read_metadata),
725        dependencies: o.objects("dependencies", |d| Dependency {
726            bundle_name: d.str("bundleName"),
727            module_name: d.str("moduleName"),
728            version_code: d.int("versionCode"),
729        }),
730        proxy_data: o.objects("proxyData", |p| ProxyData {
731            uri: p.str("uri"),
732            required_read_permission: p.str("requiredReadPermission"),
733            required_write_permission: p.str("requiredWritePermission"),
734            metadata: p.object("metadata", read_metadata),
735        }),
736        query_schemes: o.strings("querySchemes"),
737        abilities: o.objects("abilities", |a| read_ability(a, AbilityKind::Ability)),
738        extension_abilities: o.objects("extensionAbilities", |a| read_ability(a, AbilityKind::Extension)),
739        request_permissions: o.objects("requestPermissions", |p| {
740            let scene = p.object("usedScene", |s| (s.strings("abilities"), s.str("when")));
741            let (used_abilities, used_when) = scene.unwrap_or_default();
742            RequestPermission { name: p.str("name"), reason: p.str("reason"), used_abilities, used_when }
743        }),
744        define_permissions: o.objects("definePermissions", |p| DefinePermission {
745            name: p.str("name"),
746            grant_mode: p.str("grantMode"),
747            available_level: p.str("availableLevel"),
748            provision_enable: p.bool("provisionEnable"),
749            distributed_scene_enable: p.bool("distributedSceneEnable"),
750            label: p.str("label"),
751            description: p.str("description"),
752        }),
753        test_runner: o.object("testRunner", |t| TestRunner { name: t.str("name"), src_path: t.str("srcPath") }),
754        atomic_service: o.object("atomicService", |a| AtomicService {
755            preloads: a.objects("preloads", |p| p.str("moduleName")).into_iter().flatten().collect(),
756        }),
757    }
758}
759
760fn read_ability(o: &mut Obj<'_, '_>, kind: AbilityKind) -> Ability {
761    o.ignore(PRESENTATION_KEYS);
762    Ability {
763        kind,
764        name: o.str("name"),
765        src_entry: o.str("srcEntry"),
766        extension_type: o.str("type"),
767        exported: o.bool("exported"),
768        permissions: o.strings("permissions"),
769        skills: o.objects("skills", read_skill),
770        launch_type: o.str("launchType"),
771        process: o.str("process"),
772        label: o.str("label"),
773        description: o.str("description"),
774        background_modes: o.strings("backgroundModes"),
775        continuable: o.bool("continuable"),
776        metadata: o.objects("metadata", read_metadata),
777        uri: o.str("uri"),
778        read_permission: o.str("readPermission"),
779        write_permission: o.str("writePermission"),
780        extension_process_mode: o.str("extensionProcessMode"),
781        data_group_ids: o.strings("dataGroupIds"),
782        remove_mission_after_terminate: o.bool("removeMissionAfterTerminate"),
783        raw: o.to_value(),
784    }
785}
786
787fn read_skill(o: &mut Obj<'_, '_>) -> Skill {
788    Skill {
789        actions: o.strings("actions"),
790        entities: o.strings("entities"),
791        uris: o.objects("uris", |u| SkillUri {
792            scheme: u.str("scheme"),
793            host: u.str("host"),
794            port: u.text("port"),
795            path: u.str("path"),
796            path_start_with: u.str("pathStartWith"),
797            path_regex: u.str("pathRegex"),
798            mime_type: u.str("type"),
799            utd: u.str("utd"),
800            max_file_supported: u.int("maxFileSupported"),
801            link_feature: u.str("linkFeature"),
802        }),
803        permissions: o.strings("permissions"),
804        domain_verify: o.bool("domainVerify"),
805    }
806}