Skip to main content

asc_hap/
pkg_context.rs

1//! `pkgContextInfo.json`: the table of every package (the module itself,
2//! HARs, npm-style dependencies, native libraries) compiled into the
3//! module's `.abc`.
4//!
5//! It is written when the build uses normalized OHM URLs, and it is what
6//! links the manifest to the code: record names and import requests in
7//! the `.abc` then read `[bundle]&<packageName>/<path>&<version>`.
8
9use serde_json::Value;
10
11use crate::HapError;
12use crate::record;
13use crate::value::{self, Findings, Line, Note, Obj};
14
15/// A parsed `pkgContextInfo.json`.
16#[derive(Debug, Clone, PartialEq)]
17#[non_exhaustive]
18pub struct PkgContextInfo {
19    /// Sorted by [`PkgContext::name`].
20    packages: Vec<PkgContext>,
21    /// Fields that were present but wrongly typed, and so skipped.
22    pub notes: Vec<Note>,
23    /// JSON paths of fields this model has no place for.
24    pub unmodelled: Vec<String>,
25    raw: Value,
26}
27
28/// One package. Strings the file leaves empty stay empty here: the file
29/// itself writes `""` for "none".
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct PkgContext {
33    /// The key of the entry: the name code refers to the package by.
34    pub name: String,
35    /// `packageName`; equal to [`PkgContext::name`] in every file seen.
36    pub package_name: String,
37    /// Owning bundle; empty for packages compiled into this module.
38    pub bundle_name: String,
39    /// `moduleName`: the module that owns the package.
40    pub module_name: String,
41    /// Empty for the module itself and for native libraries.
42    pub version: String,
43    /// The package's entry file relative to its root (`Index.ets`), or
44    /// for the module's own package its source root (`src/main/`).
45    pub entry_path: String,
46    /// A native library (`libfoo.so`) rather than compiled code.
47    pub is_so: bool,
48    /// `dependencyAlias`: the alias the package is declared under as a dependency; empty when none.
49    pub dependency_alias: String,
50}
51
52impl PkgContext {
53    /// The record a bare import of this package resolves to:
54    /// `&<name>/<entryPath without extension>&<version>`. `None` for
55    /// native libraries and packages without an entry file.
56    ///
57    /// The record may be absent from the `.abc` when nothing imports the
58    /// package's entry.
59    pub fn entry_record_name(&self) -> Option<String> {
60        if self.is_so || self.entry_path.is_empty() || self.entry_path.ends_with('/') {
61            return None;
62        }
63        Some(format!("&{}/{}&{}", self.name, record::strip_source_extension(&self.entry_path), self.version))
64    }
65}
66
67/// A normalized record name split against the package table.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69#[non_exhaustive]
70pub struct RecordOwner<'c, 'r> {
71    /// The entry of the package table the record belongs to.
72    pub package: &'c PkgContext,
73    /// Text before the first `&`; empty for records of this bundle.
74    pub bundle: &'r str,
75    /// Path inside the package, without extension; empty when the record
76    /// is the package name alone.
77    pub path: &'r str,
78    /// Text after the last `&`.
79    pub version: &'r str,
80}
81
82impl PkgContextInfo {
83    /// Read a `pkgContextInfo.json`.
84    ///
85    /// # Errors
86    ///
87    /// Only when `bytes` is not JSON or its root is not an object.
88    pub fn parse(bytes: &[u8]) -> Result<Self, HapError> {
89        let map = value::parse_object(bytes)?;
90        let mut findings = Findings::default();
91        let mut packages = Vec::with_capacity(map.len());
92        for (name, entry) in &map {
93            let Some(entry) = entry.as_object() else {
94                findings.notes.push(Note { path: name.clone(), message: format!("expected object, found {}", value::kind(entry)) });
95                continue;
96            };
97            // Each entry is read as its own root so that note paths start
98            // at the package name.
99            let mut scratch = Findings::default();
100            let mut o = Obj::root(entry, &mut scratch);
101            packages.push(PkgContext {
102                name: name.clone(),
103                package_name: o.str("packageName").unwrap_or_default(),
104                bundle_name: o.str("bundleName").unwrap_or_default(),
105                module_name: o.str("moduleName").unwrap_or_default(),
106                version: o.text("version").unwrap_or_default(),
107                entry_path: o.str("entryPath").unwrap_or_default(),
108                is_so: o.bool("isSO").unwrap_or_default(),
109                dependency_alias: o.str("dependencyAlias").unwrap_or_default(),
110            });
111            o.finish();
112            findings.notes.extend(scratch.notes.into_iter().map(|n| Note { path: format!("{name}.{}", n.path), ..n }));
113            findings.unmodelled.extend(scratch.unmodelled.into_iter().map(|path| format!("{name}.{path}")));
114        }
115        packages.sort_by(|a, b| a.name.cmp(&b.name));
116        Ok(PkgContextInfo { packages, notes: findings.notes, unmodelled: findings.unmodelled, raw: Value::Object(map) })
117    }
118
119    /// Every package, sorted by name.
120    pub fn packages(&self) -> &[PkgContext] {
121        &self.packages
122    }
123
124    /// The package code refers to as `name`.
125    pub fn get(&self, name: &str) -> Option<&PkgContext> {
126        self.packages.binary_search_by(|p| p.name.as_str().cmp(name)).ok().map(|i| &self.packages[i])
127    }
128
129    /// Split a normalized record name (`&@ohos/crypto-js/src/main/js/aes&2.0.0`)
130    /// and find the package it belongs to: the longest package name that
131    /// is a whole-segment prefix of the part between the `&`s. Import
132    /// requests work too once their `@normalized:N&<module>&` head is cut
133    /// off.
134    ///
135    /// `None` for names without two `&` (the `bundle/module/ets/…` form)
136    /// and for packages missing from the table.
137    pub fn locate<'r>(&self, record: &'r str) -> Option<RecordOwner<'_, 'r>> {
138        let (bundle, rest) = record.split_once('&')?;
139        let (body, version) = rest.rsplit_once('&')?;
140        let boundaries = body.match_indices('/').map(|(i, _)| i).chain([body.len()]).collect::<Vec<_>>();
141        boundaries.into_iter().rev().find_map(|end| {
142            let package = self.get(&body[..end])?;
143            Some(RecordOwner { package, bundle, path: body[end..].trim_start_matches('/'), version })
144        })
145    }
146
147    /// The whole document as parsed.
148    pub fn raw(&self) -> &Value {
149        &self.raw
150    }
151
152    /// The document pretty-printed.
153    pub fn pretty(&self) -> String {
154        value::pretty(&self.raw)
155    }
156
157    /// One `package` line per package, sorted by name, then `unmodelled`
158    /// and `note` lines.
159    pub fn summary_lines(&self) -> Vec<String> {
160        let text = |s: &str| if s.is_empty() { "-".to_owned() } else { value::token(s).into_owned() };
161        let mut out: Vec<String> = self
162            .packages
163            .iter()
164            .map(|p| {
165                let mut line = Line::new("package")
166                    .word(&p.name)
167                    .raw("version", text(&p.version))
168                    .raw("entryPath", text(&p.entry_path))
169                    .raw("isSO", p.is_so)
170                    .raw("bundleName", text(&p.bundle_name))
171                    .raw("moduleName", text(&p.module_name))
172                    .raw("dependencyAlias", text(&p.dependency_alias));
173                if p.package_name != p.name {
174                    line = line.raw("packageName", text(&p.package_name));
175                }
176                line.end()
177            })
178            .collect();
179        out.extend(self.unmodelled.iter().map(|path| Line::new("unmodelled").word(path).end()));
180        out.extend(self.notes.iter().map(|note| format!("note {note}")));
181        out
182    }
183
184    /// [`PkgContextInfo::summary_lines`] joined, each line
185    /// newline-terminated.
186    pub fn summary(&self) -> String {
187        crate::join_lines(&self.summary_lines())
188    }
189}