use std::fmt;
use serde_json::Value;
use crate::HapError;
use crate::value::{self, Findings, Line, Note, Obj};
#[derive(Debug, Clone, PartialEq)]
pub struct ModuleJson {
pub app: App,
pub module: Module,
pub notes: Vec<Note>,
pub unmodelled: Vec<String>,
raw: Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ApiVersion(pub i64);
impl ApiVersion {
pub fn level(self) -> i64 {
self.0 % 1000
}
}
impl fmt::Display for ApiVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.level() == self.0 { write!(f, "{}", self.0) } else { write!(f, "{}({})", self.0, self.level()) }
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct App {
pub bundle_name: Option<String>,
pub bundle_type: Option<String>,
pub version_code: Option<i64>,
pub version_name: Option<String>,
pub min_compatible_version_code: Option<i64>,
pub vendor: Option<String>,
pub debug: Option<bool>,
pub min_api_version: Option<ApiVersion>,
pub target_api_version: Option<ApiVersion>,
pub target_minor_api_version: Option<i64>,
pub target_patch_api_version: Option<i64>,
pub min_minor_api_version: Option<i64>,
pub min_patch_api_version: Option<i64>,
pub api_release_type: Option<String>,
pub compile_sdk_type: Option<String>,
pub compile_sdk_version: Option<String>,
pub build_mode: Option<String>,
pub label: Option<String>,
pub description: Option<String>,
pub environments: Vec<NameValue>,
pub multi_app_mode: Option<MultiAppMode>,
pub gwp_asan_enabled: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MultiAppMode {
pub kind: Option<String>,
pub max_count: Option<i64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NameValue {
pub name: Option<String>,
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Metadata {
pub name: Option<String>,
pub value: Option<String>,
pub resource: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Module {
pub name: Option<String>,
pub kind: Option<String>,
pub package_name: Option<String>,
pub src_entry: Option<String>,
pub main_element: Option<String>,
pub description: Option<String>,
pub process: Option<String>,
pub device_types: Vec<String>,
pub virtual_machine: Option<String>,
pub compile_mode: Option<String>,
pub installation_free: Option<bool>,
pub delivery_with_install: Option<bool>,
pub compress_native_libs: Option<bool>,
pub isolation_mode: Option<String>,
pub pages: Option<String>,
pub router_map: Option<String>,
pub app_startup: Option<String>,
pub target_module_name: Option<String>,
pub target_priority: Option<i64>,
pub metadata: Vec<Metadata>,
pub dependencies: Vec<Dependency>,
pub proxy_data: Vec<ProxyData>,
pub query_schemes: Vec<String>,
pub abilities: Vec<Ability>,
pub extension_abilities: Vec<Ability>,
pub request_permissions: Vec<RequestPermission>,
pub define_permissions: Vec<DefinePermission>,
pub test_runner: Option<TestRunner>,
pub atomic_service: Option<AtomicService>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Dependency {
pub bundle_name: Option<String>,
pub module_name: Option<String>,
pub version_code: Option<i64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ProxyData {
pub uri: Option<String>,
pub required_read_permission: Option<String>,
pub required_write_permission: Option<String>,
pub metadata: Option<Metadata>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum AbilityKind {
#[default]
Ability,
Extension,
}
impl AbilityKind {
pub fn as_str(self) -> &'static str {
match self {
AbilityKind::Ability => "ability",
AbilityKind::Extension => "extension",
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Ability {
pub kind: AbilityKind,
pub name: Option<String>,
pub src_entry: Option<String>,
pub extension_type: Option<String>,
pub exported: Option<bool>,
pub permissions: Vec<String>,
pub skills: Vec<Skill>,
pub launch_type: Option<String>,
pub process: Option<String>,
pub label: Option<String>,
pub description: Option<String>,
pub background_modes: Vec<String>,
pub continuable: Option<bool>,
pub metadata: Vec<Metadata>,
pub uri: Option<String>,
pub read_permission: Option<String>,
pub write_permission: Option<String>,
pub extension_process_mode: Option<String>,
pub data_group_ids: Vec<String>,
pub remove_mission_after_terminate: Option<bool>,
pub raw: Value,
}
impl Ability {
pub fn is_exported(&self) -> bool {
self.exported == Some(true)
}
pub fn deep_links(&self) -> Vec<String> {
let mut links: Vec<String> = Vec::new();
for link in self.skills.iter().flat_map(|s| &s.uris).flat_map(SkillUri::patterns) {
if !links.contains(&link) {
links.push(link);
}
}
links
}
fn display_name(&self) -> &str {
self.name.as_deref().unwrap_or("-")
}
fn line(&self) -> String {
let reason = if self.exported.is_some() { "explicit" } else { "default" };
let mut line = Line::new(self.kind.as_str()).word(self.display_name());
if self.kind == AbilityKind::Extension || self.extension_type.is_some() {
line = line.opt("type", self.extension_type.as_deref());
}
line = line
.raw("exported", format_args!("{} ({reason})", self.is_exported()))
.list("permissions", &self.permissions)
.raw("skills", self.skills.len())
.opt("srcEntry", self.src_entry.as_deref());
if self.uri.is_some() || self.read_permission.is_some() || self.write_permission.is_some() {
line = line
.opt("uri", self.uri.as_deref())
.opt("readPermission", self.read_permission.as_deref())
.opt("writePermission", self.write_permission.as_deref());
}
line.opt_present("launchType", self.launch_type.as_deref())
.opt_present("process", self.process.as_deref())
.opt_present("extensionProcessMode", self.extension_process_mode.as_deref())
.flag_present("continuable", self.continuable)
.list_present("backgroundModes", &self.background_modes)
.list_present("dataGroupIds", &self.data_group_ids)
.flag_present("removeMissionAfterTerminate", self.remove_mission_after_terminate)
.end()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Skill {
pub actions: Vec<String>,
pub entities: Vec<String>,
pub uris: Vec<SkillUri>,
pub permissions: Vec<String>,
pub domain_verify: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SkillUri {
pub scheme: Option<String>,
pub host: Option<String>,
pub port: Option<String>,
pub path: Option<String>,
pub path_start_with: Option<String>,
pub path_regex: Option<String>,
pub mime_type: Option<String>,
pub utd: Option<String>,
pub max_file_supported: Option<i64>,
pub link_feature: Option<String>,
}
impl SkillUri {
pub fn patterns(&self) -> Vec<String> {
let Some(scheme) = &self.scheme else {
return Vec::new();
};
let paths: Vec<String> = [
self.path.as_ref().map(|p| p.trim_start_matches('/').to_owned()),
self.path_start_with.as_ref().map(|p| format!("{}*", p.trim_start_matches('/'))),
self.path_regex.as_ref().map(|p| format!("regex:{p}")),
]
.into_iter()
.flatten()
.collect();
let authority = match (&self.host, &self.port) {
(Some(host), Some(port)) => format!("{host}:{port}"),
(Some(host), None) => host.clone(),
(None, Some(port)) => format!("*:{port}"),
(None, None) if paths.is_empty() => return vec![format!("{scheme}://*")],
(None, None) => "*".to_owned(),
};
if paths.is_empty() {
return vec![format!("{scheme}://{authority}")];
}
paths.iter().map(|path| format!("{scheme}://{authority}/{path}")).collect()
}
fn line(&self, owner: &str, skill: usize) -> String {
Line::new("uri")
.word(owner)
.raw("skill", skill)
.opt_present("scheme", self.scheme.as_deref())
.opt_present("host", self.host.as_deref())
.opt_present("port", self.port.as_deref())
.opt_present("path", self.path.as_deref())
.opt_present("pathStartWith", self.path_start_with.as_deref())
.opt_present("pathRegex", self.path_regex.as_deref())
.opt_present("type", self.mime_type.as_deref())
.opt_present("utd", self.utd.as_deref())
.opt_present("maxFileSupported", self.max_file_supported.map(|n| n.to_string()).as_deref())
.opt_present("linkFeature", self.link_feature.as_deref())
.end()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RequestPermission {
pub name: Option<String>,
pub reason: Option<String>,
pub used_abilities: Vec<String>,
pub used_when: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DefinePermission {
pub name: Option<String>,
pub grant_mode: Option<String>,
pub available_level: Option<String>,
pub provision_enable: Option<bool>,
pub distributed_scene_enable: Option<bool>,
pub label: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TestRunner {
pub name: Option<String>,
pub src_path: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AtomicService {
pub preloads: Vec<String>,
}
const PRESENTATION_KEYS: &[&str] = &[
"icon",
"startWindow",
"startWindowIcon",
"startWindowBackground",
"orientation",
"supportWindowMode",
"maxWindowRatio",
"minWindowRatio",
"maxWindowWidth",
"minWindowWidth",
"maxWindowHeight",
"minWindowHeight",
];
impl ModuleJson {
pub fn parse(bytes: &[u8]) -> Result<Self, HapError> {
let map = value::parse_object(bytes)?;
let mut findings = Findings::default();
let mut root = Obj::root(&map, &mut findings);
let app = root.object("app", read_app).unwrap_or_default();
let module = root.object("module", read_module).unwrap_or_default();
root.finish();
Ok(ModuleJson { app, module, notes: findings.notes, unmodelled: findings.unmodelled, raw: Value::Object(map) })
}
pub fn raw(&self) -> &Value {
&self.raw
}
pub fn pretty(&self) -> String {
value::pretty(&self.raw)
}
pub fn components(&self) -> impl Iterator<Item = &Ability> {
self.module.abilities.iter().chain(&self.module.extension_abilities)
}
pub fn summary_lines(&self) -> Vec<String> {
let app = &self.app;
let module = &self.module;
let mut bundle = Line::new("bundle")
.word(app.bundle_name.as_deref().unwrap_or("-"))
.opt("bundleType", app.bundle_type.as_deref())
.num("versionCode", app.version_code)
.opt("versionName", app.version_name.as_deref())
.opt("vendor", app.vendor.as_deref())
.flag("debug", app.debug)
.num("minAPIVersion", app.min_api_version)
.num("targetAPIVersion", app.target_api_version)
.opt("apiReleaseType", app.api_release_type.as_deref())
.opt("compileSdkType", app.compile_sdk_type.as_deref())
.opt("compileSdkVersion", app.compile_sdk_version.as_deref())
.opt("buildMode", app.build_mode.as_deref());
if let Some(mode) = &app.multi_app_mode {
bundle = bundle.opt("multiAppMode", mode.kind.as_deref()).num("multiAppMaxCount", mode.max_count);
}
let mut out = vec![bundle.flag_present("GWPAsanEnabled", app.gwp_asan_enabled).end()];
out.extend(
app.environments.iter().map(|e| Line::new("app-env").opt("name", e.name.as_deref()).opt("value", e.value.as_deref()).end()),
);
out.push(
Line::new("module")
.word(module.name.as_deref().unwrap_or("-"))
.opt("type", module.kind.as_deref())
.opt("packageName", module.package_name.as_deref())
.opt("mainElement", module.main_element.as_deref())
.opt("srcEntry", module.src_entry.as_deref())
.list("deviceTypes", &module.device_types)
.opt("virtualMachine", module.virtual_machine.as_deref())
.opt("compileMode", module.compile_mode.as_deref())
.flag("installationFree", module.installation_free)
.flag("deliveryWithInstall", module.delivery_with_install)
.opt("process", module.process.as_deref())
.opt("pages", module.pages.as_deref())
.opt_present("routerMap", module.router_map.as_deref())
.opt_present("appStartup", module.app_startup.as_deref())
.flag_present("compressNativeLibs", module.compress_native_libs)
.opt_present("isolationMode", module.isolation_mode.as_deref())
.opt_present("targetModuleName", module.target_module_name.as_deref())
.opt_present("targetPriority", module.target_priority.map(|n| n.to_string()).as_deref())
.end(),
);
out.extend(module.metadata.iter().map(|m| metadata_line(Line::new("module-metadata"), m)));
out.extend(module.dependencies.iter().map(|d| {
Line::new("dependency")
.opt("bundleName", d.bundle_name.as_deref())
.opt("moduleName", d.module_name.as_deref())
.num("versionCode", d.version_code)
.end()
}));
out.extend(module.query_schemes.iter().map(|s| Line::new("query-scheme").word(s).end()));
out.extend(module.proxy_data.iter().map(|p| {
let line = Line::new("proxy-data")
.opt("uri", p.uri.as_deref())
.opt("requiredReadPermission", p.required_read_permission.as_deref())
.opt("requiredWritePermission", p.required_write_permission.as_deref());
match &p.metadata {
Some(m) => line.opt("metadataName", m.name.as_deref()).opt("metadataResource", m.resource.as_deref()).end(),
None => line.end(),
}
}));
out.extend(module.request_permissions.iter().map(|p| {
Line::new("request-permission")
.word(p.name.as_deref().unwrap_or("-"))
.opt("reason", p.reason.as_deref())
.opt("when", p.used_when.as_deref())
.list("abilities", &p.used_abilities)
.end()
}));
out.extend(module.define_permissions.iter().map(|p| {
Line::new("define-permission")
.word(p.name.as_deref().unwrap_or("-"))
.opt("grantMode", p.grant_mode.as_deref())
.opt("availableLevel", p.available_level.as_deref())
.flag("provisionEnable", p.provision_enable)
.flag("distributedSceneEnable", p.distributed_scene_enable)
.end()
}));
if let Some(t) = &module.test_runner {
out.push(Line::new("test-runner").opt("name", t.name.as_deref()).opt("srcPath", t.src_path.as_deref()).end());
}
if let Some(a) = &module.atomic_service {
out.push(Line::new("atomic-service").list("preloads", &a.preloads).end());
}
for component in self.components() {
let name = component.display_name();
out.push(component.line());
for (i, skill) in component.skills.iter().enumerate() {
let number = i + 1;
out.push(
Line::new("skill")
.word(name)
.raw("skill", number)
.list("actions", &skill.actions)
.list("entities", &skill.entities)
.raw("uris", skill.uris.len())
.list("permissions", &skill.permissions)
.flag_present("domainVerify", skill.domain_verify)
.end(),
);
out.extend(skill.uris.iter().map(|u| u.line(name, number)));
}
out.extend(component.deep_links().iter().map(|l| Line::new("deeplink").word(name).word(l).end()));
out.extend(component.metadata.iter().map(|m| metadata_line(Line::new("metadata").word(name), m)));
}
out.extend(self.unmodelled.iter().map(|path| Line::new("unmodelled").word(path).end()));
out.extend(self.notes.iter().map(|note| format!("note {note}")));
out
}
pub fn summary(&self) -> String {
crate::join_lines(&self.summary_lines())
}
}
fn metadata_line(line: Line, m: &Metadata) -> String {
line.opt("name", m.name.as_deref()).opt_present("value", m.value.as_deref()).opt_present("resource", m.resource.as_deref()).end()
}
fn read_app(o: &mut Obj<'_, '_>) -> App {
o.ignore(&["icon"]);
App {
bundle_name: o.str("bundleName"),
bundle_type: o.str("bundleType"),
version_code: o.int("versionCode"),
version_name: o.str("versionName"),
min_compatible_version_code: o.int("minCompatibleVersionCode"),
vendor: o.str("vendor"),
debug: o.bool("debug"),
min_api_version: o.int("minAPIVersion").map(ApiVersion),
target_api_version: o.int("targetAPIVersion").map(ApiVersion),
target_minor_api_version: o.int("targetMinorAPIVersion"),
target_patch_api_version: o.int("targetPatchAPIVersion"),
min_minor_api_version: o.int("minMinorAPIVersion"),
min_patch_api_version: o.int("minPatchAPIVersion"),
api_release_type: o.str("apiReleaseType"),
compile_sdk_type: o.str("compileSdkType"),
compile_sdk_version: o.text("compileSdkVersion"),
build_mode: o.str("buildMode"),
label: o.str("label"),
description: o.str("description"),
environments: o.objects("appEnvironments", |e| NameValue { name: e.str("name"), value: e.text("value") }),
multi_app_mode: o.object("multiAppMode", |m| MultiAppMode { kind: m.str("multiAppModeType"), max_count: m.int("maxCount") }),
gwp_asan_enabled: o.bool("GWPAsanEnabled"),
}
}
fn read_metadata(o: &mut Obj<'_, '_>) -> Metadata {
Metadata { name: o.str("name"), value: o.text("value"), resource: o.str("resource") }
}
fn read_module(o: &mut Obj<'_, '_>) -> Module {
Module {
name: o.str("name"),
kind: o.str("type"),
package_name: o.str("packageName"),
src_entry: o.str("srcEntry"),
main_element: o.str("mainElement"),
description: o.str("description"),
process: o.str("process"),
device_types: o.strings("deviceTypes"),
virtual_machine: o.str("virtualMachine"),
compile_mode: o.str("compileMode"),
installation_free: o.bool("installationFree"),
delivery_with_install: o.bool("deliveryWithInstall"),
compress_native_libs: o.bool("compressNativeLibs"),
isolation_mode: o.str("isolationMode"),
pages: o.str("pages"),
router_map: o.str("routerMap"),
app_startup: o.str("appStartup"),
target_module_name: o.str("targetModuleName"),
target_priority: o.int("targetPriority"),
metadata: o.objects("metadata", read_metadata),
dependencies: o.objects("dependencies", |d| Dependency {
bundle_name: d.str("bundleName"),
module_name: d.str("moduleName"),
version_code: d.int("versionCode"),
}),
proxy_data: o.objects("proxyData", |p| ProxyData {
uri: p.str("uri"),
required_read_permission: p.str("requiredReadPermission"),
required_write_permission: p.str("requiredWritePermission"),
metadata: p.object("metadata", read_metadata),
}),
query_schemes: o.strings("querySchemes"),
abilities: o.objects("abilities", |a| read_ability(a, AbilityKind::Ability)),
extension_abilities: o.objects("extensionAbilities", |a| read_ability(a, AbilityKind::Extension)),
request_permissions: o.objects("requestPermissions", |p| {
let scene = p.object("usedScene", |s| (s.strings("abilities"), s.str("when")));
let (used_abilities, used_when) = scene.unwrap_or_default();
RequestPermission { name: p.str("name"), reason: p.str("reason"), used_abilities, used_when }
}),
define_permissions: o.objects("definePermissions", |p| DefinePermission {
name: p.str("name"),
grant_mode: p.str("grantMode"),
available_level: p.str("availableLevel"),
provision_enable: p.bool("provisionEnable"),
distributed_scene_enable: p.bool("distributedSceneEnable"),
label: p.str("label"),
description: p.str("description"),
}),
test_runner: o.object("testRunner", |t| TestRunner { name: t.str("name"), src_path: t.str("srcPath") }),
atomic_service: o.object("atomicService", |a| AtomicService {
preloads: a.objects("preloads", |p| p.str("moduleName")).into_iter().flatten().collect(),
}),
}
}
fn read_ability(o: &mut Obj<'_, '_>, kind: AbilityKind) -> Ability {
o.ignore(PRESENTATION_KEYS);
Ability {
kind,
name: o.str("name"),
src_entry: o.str("srcEntry"),
extension_type: o.str("type"),
exported: o.bool("exported"),
permissions: o.strings("permissions"),
skills: o.objects("skills", read_skill),
launch_type: o.str("launchType"),
process: o.str("process"),
label: o.str("label"),
description: o.str("description"),
background_modes: o.strings("backgroundModes"),
continuable: o.bool("continuable"),
metadata: o.objects("metadata", read_metadata),
uri: o.str("uri"),
read_permission: o.str("readPermission"),
write_permission: o.str("writePermission"),
extension_process_mode: o.str("extensionProcessMode"),
data_group_ids: o.strings("dataGroupIds"),
remove_mission_after_terminate: o.bool("removeMissionAfterTerminate"),
raw: o.to_value(),
}
}
fn read_skill(o: &mut Obj<'_, '_>) -> Skill {
Skill {
actions: o.strings("actions"),
entities: o.strings("entities"),
uris: o.objects("uris", |u| SkillUri {
scheme: u.str("scheme"),
host: u.str("host"),
port: u.text("port"),
path: u.str("path"),
path_start_with: u.str("pathStartWith"),
path_regex: u.str("pathRegex"),
mime_type: u.str("type"),
utd: u.str("utd"),
max_file_supported: u.int("maxFileSupported"),
link_feature: u.str("linkFeature"),
}),
permissions: o.strings("permissions"),
domain_verify: o.bool("domainVerify"),
}
}