1use serde_json::Value;
10
11use crate::HapError;
12use crate::record;
13use crate::value::{self, Findings, Line, Note, Obj};
14
15#[derive(Debug, Clone, PartialEq)]
17#[non_exhaustive]
18pub struct PkgContextInfo {
19 packages: Vec<PkgContext>,
21 pub notes: Vec<Note>,
23 pub unmodelled: Vec<String>,
25 raw: Value,
26}
27
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct PkgContext {
33 pub name: String,
35 pub package_name: String,
37 pub bundle_name: String,
39 pub module_name: String,
41 pub version: String,
43 pub entry_path: String,
46 pub is_so: bool,
48 pub dependency_alias: String,
50}
51
52impl PkgContext {
53 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69#[non_exhaustive]
70pub struct RecordOwner<'c, 'r> {
71 pub package: &'c PkgContext,
73 pub bundle: &'r str,
75 pub path: &'r str,
78 pub version: &'r str,
80}
81
82impl PkgContextInfo {
83 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 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 pub fn packages(&self) -> &[PkgContext] {
121 &self.packages
122 }
123
124 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 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 pub fn raw(&self) -> &Value {
149 &self.raw
150 }
151
152 pub fn pretty(&self) -> String {
154 value::pretty(&self.raw)
155 }
156
157 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 pub fn summary(&self) -> String {
187 crate::join_lines(&self.summary_lines())
188 }
189}