Skip to main content

asc_hap/
pack.rs

1//! `pack.info`: the packing tool's digest of a HAP (one module) or of an
2//! `.app` bundle (every module), written at the archive root.
3
4use serde_json::Value;
5
6use crate::HapError;
7use crate::value::{self, Findings, Line, Note, Obj};
8
9/// A parsed `pack.info`.
10#[derive(Debug, Clone, PartialEq)]
11#[non_exhaustive]
12pub struct PackInfo {
13    /// `summary.app`.
14    pub app: PackApp,
15    /// `summary.modules`.
16    pub modules: Vec<PackModule>,
17    /// `packages`: the HAP/HSP files of the bundle.
18    pub packages: Vec<PackPackage>,
19    /// Fields that were present but wrongly typed, and so skipped.
20    pub notes: Vec<Note>,
21    /// JSON paths of fields this model has no place for.
22    pub unmodelled: Vec<String>,
23    raw: Value,
24}
25
26/// `summary.app`: bundle-wide facts.
27#[derive(Debug, Clone, Default, PartialEq, Eq)]
28#[non_exhaustive]
29pub struct PackApp {
30    /// `bundleName`.
31    pub bundle_name: Option<String>,
32    /// `bundleType`: `app`, `atomicService`, `shared`…
33    pub bundle_type: Option<String>,
34    /// `version.code`.
35    pub version_code: Option<i64>,
36    /// `version.name`.
37    pub version_name: Option<String>,
38}
39
40/// An entry of `summary.modules`.
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
42#[non_exhaustive]
43pub struct PackModule {
44    /// `distro.moduleName`.
45    pub name: Option<String>,
46    /// `distro.moduleType`.
47    pub kind: Option<String>,
48    /// `distro.installationFree`: whether the module runs without installation.
49    pub installation_free: Option<bool>,
50    /// `distro.deliveryWithInstall`: whether the module is installed with the app.
51    pub delivery_with_install: Option<bool>,
52    /// `mainAbility`: the ability that starts first.
53    pub main_ability: Option<String>,
54    /// `deviceType`: `phone`, `tablet`, `2in1`…
55    pub device_types: Vec<String>,
56    /// `apiVersion.compatible`: the minimum API level.
57    pub api_compatible: Option<i64>,
58    /// `apiVersion.target`.
59    pub api_target: Option<i64>,
60    /// `apiVersion.releaseType`.
61    pub api_release_type: Option<String>,
62    /// `abilities`.
63    pub abilities: Vec<PackAbility>,
64    /// `extensionAbilities`.
65    pub extension_abilities: Vec<PackExtension>,
66}
67
68/// An entry of a module's `abilities`.
69#[derive(Debug, Clone, Default, PartialEq, Eq)]
70#[non_exhaustive]
71pub struct PackAbility {
72    /// `name`.
73    pub name: Option<String>,
74    /// `label`: usually a `$string:` resource reference.
75    pub label: Option<String>,
76}
77
78/// An entry of a module's `extensionAbilities`.
79#[derive(Debug, Clone, Default, PartialEq, Eq)]
80#[non_exhaustive]
81pub struct PackExtension {
82    /// `name`.
83    pub name: Option<String>,
84    /// Service widgets (cards) a form extension offers.
85    pub forms: Vec<PackForm>,
86}
87
88/// An entry of an extension's `forms`: one service widget.
89#[derive(Debug, Clone, Default, PartialEq, Eq)]
90#[non_exhaustive]
91pub struct PackForm {
92    /// `name`.
93    pub name: Option<String>,
94    /// `type`: `JS` or `eTS`.
95    pub kind: Option<String>,
96    /// `updateEnabled`: whether the widget refreshes periodically.
97    pub update_enabled: Option<bool>,
98    /// `updateDuration`: the refresh period, in units of 30 minutes.
99    pub update_duration: Option<i64>,
100    /// `scheduledUpdateTime`: the time of day of the daily update, `10:30`.
101    pub scheduled_update_time: Option<String>,
102    /// `defaultDimension`: the default size, e.g. `2*2`.
103    pub default_dimension: Option<String>,
104    /// `supportDimensions`: every size offered.
105    pub support_dimensions: Vec<String>,
106}
107
108/// An entry of `packages`: one HAP/HSP file of the bundle.
109#[derive(Debug, Clone, Default, PartialEq, Eq)]
110#[non_exhaustive]
111pub struct PackPackage {
112    /// File stem of the package inside the bundle (`entry-default`).
113    pub name: Option<String>,
114    /// `moduleType`: `entry`, `feature` or `shared`.
115    pub module_type: Option<String>,
116    /// `deviceType`.
117    pub device_types: Vec<String>,
118    /// `deliveryWithInstall`: whether the package is installed with the app.
119    pub delivery_with_install: Option<bool>,
120}
121
122impl PackInfo {
123    /// Read a `pack.info`.
124    ///
125    /// # Errors
126    ///
127    /// Only when `bytes` is not JSON or its root is not an object.
128    pub fn parse(bytes: &[u8]) -> Result<Self, HapError> {
129        let map = value::parse_object(bytes)?;
130        let mut findings = Findings::default();
131        let mut root = Obj::root(&map, &mut findings);
132        let summary = root.object("summary", |s| (s.object("app", read_app).unwrap_or_default(), s.objects("modules", read_module)));
133        let (app, modules) = summary.unwrap_or_default();
134        let packages = root.objects("packages", |p| PackPackage {
135            name: p.str("name"),
136            module_type: p.str("moduleType"),
137            device_types: p.strings("deviceType"),
138            delivery_with_install: p.bool("deliveryWithInstall"),
139        });
140        root.finish();
141        Ok(PackInfo { app, modules, packages, notes: findings.notes, unmodelled: findings.unmodelled, raw: Value::Object(map) })
142    }
143
144    /// The whole document as parsed.
145    pub fn raw(&self) -> &Value {
146        &self.raw
147    }
148
149    /// The document pretty-printed.
150    pub fn pretty(&self) -> String {
151        value::pretty(&self.raw)
152    }
153
154    /// One fact per line: `pack-app`, then per module `pack-module`,
155    /// `pack-ability`, `pack-extension` and `pack-form` (each carrying the
156    /// module name), then `pack-package`, `unmodelled` and `note`.
157    pub fn summary_lines(&self) -> Vec<String> {
158        let mut out = vec![
159            Line::new("pack-app")
160                .word(self.app.bundle_name.as_deref().unwrap_or("-"))
161                .opt("bundleType", self.app.bundle_type.as_deref())
162                .num("versionCode", self.app.version_code)
163                .opt("versionName", self.app.version_name.as_deref())
164                .end(),
165        ];
166        for module in &self.modules {
167            let name = module.name.as_deref().unwrap_or("-");
168            out.push(
169                Line::new("pack-module")
170                    .word(name)
171                    .opt("type", module.kind.as_deref())
172                    .opt("mainAbility", module.main_ability.as_deref())
173                    .list("deviceTypes", &module.device_types)
174                    .flag("installationFree", module.installation_free)
175                    .flag("deliveryWithInstall", module.delivery_with_install)
176                    .num("apiCompatible", module.api_compatible)
177                    .num("apiTarget", module.api_target)
178                    .opt("apiReleaseType", module.api_release_type.as_deref())
179                    .end(),
180            );
181            out.extend(module.abilities.iter().map(|a| {
182                Line::new("pack-ability").word(name).word(a.name.as_deref().unwrap_or("-")).opt("label", a.label.as_deref()).end()
183            }));
184            for extension in &module.extension_abilities {
185                let extension_name = extension.name.as_deref().unwrap_or("-");
186                out.push(Line::new("pack-extension").word(name).word(extension_name).raw("forms", extension.forms.len()).end());
187                out.extend(extension.forms.iter().map(|f| {
188                    Line::new("pack-form")
189                        .word(name)
190                        .word(extension_name)
191                        .word(f.name.as_deref().unwrap_or("-"))
192                        .opt("type", f.kind.as_deref())
193                        .flag("updateEnabled", f.update_enabled)
194                        .num("updateDuration", f.update_duration)
195                        .opt_present("scheduledUpdateTime", f.scheduled_update_time.as_deref())
196                        .opt("defaultDimension", f.default_dimension.as_deref())
197                        .list("supportDimensions", &f.support_dimensions)
198                        .end()
199                }));
200            }
201        }
202        out.extend(self.packages.iter().map(|p| {
203            Line::new("pack-package")
204                .word(p.name.as_deref().unwrap_or("-"))
205                .opt("moduleType", p.module_type.as_deref())
206                .list("deviceTypes", &p.device_types)
207                .flag("deliveryWithInstall", p.delivery_with_install)
208                .end()
209        }));
210        out.extend(self.unmodelled.iter().map(|path| Line::new("unmodelled").word(path).end()));
211        out.extend(self.notes.iter().map(|note| format!("note {note}")));
212        out
213    }
214
215    /// [`PackInfo::summary_lines`] joined, each line newline-terminated.
216    pub fn summary(&self) -> String {
217        crate::join_lines(&self.summary_lines())
218    }
219}
220
221fn read_app(o: &mut Obj<'_, '_>) -> PackApp {
222    let version = o.object("version", |v| (v.int("code"), v.str("name")));
223    let (version_code, version_name) = version.unwrap_or_default();
224    PackApp { bundle_name: o.str("bundleName"), bundle_type: o.str("bundleType"), version_code, version_name }
225}
226
227fn read_module(o: &mut Obj<'_, '_>) -> PackModule {
228    let distro =
229        o.object("distro", |d| (d.str("moduleName"), d.str("moduleType"), d.bool("installationFree"), d.bool("deliveryWithInstall")));
230    let (name, kind, installation_free, delivery_with_install) = distro.unwrap_or_default();
231    let api = o.object("apiVersion", |a| (a.int("compatible"), a.int("target"), a.str("releaseType")));
232    let (api_compatible, api_target, api_release_type) = api.unwrap_or_default();
233    PackModule {
234        name,
235        kind,
236        installation_free,
237        delivery_with_install,
238        main_ability: o.str("mainAbility"),
239        device_types: o.strings("deviceType"),
240        api_compatible,
241        api_target,
242        api_release_type,
243        abilities: o.objects("abilities", |a| PackAbility { name: a.str("name"), label: a.str("label") }),
244        extension_abilities: o.objects("extensionAbilities", |e| PackExtension {
245            name: e.str("name"),
246            forms: e.objects("forms", |f| PackForm {
247                name: f.str("name"),
248                kind: f.str("type"),
249                update_enabled: f.bool("updateEnabled"),
250                update_duration: f.int("updateDuration"),
251                scheduled_update_time: f.str("scheduledUpdateTime"),
252                default_dimension: f.str("defaultDimension"),
253                support_dimensions: f.strings("supportDimensions"),
254            }),
255        }),
256    }
257}