1use super::{
2 InstalledMetadataError, ValueKindName, ViewError, decode_optional, decode_required, duplicate,
3 expect_list, missing, named_values, unexpected_length, unexpected_type,
4};
5use crate::{RObject, RValue};
6use std::{collections::BTreeMap, fmt};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct PackageMeta {
11 description: BTreeMap<String, Option<String>>,
12 built: Option<Built>,
13}
14
15impl PackageMeta {
16 pub fn read_installed(
19 package_dir: impl AsRef<std::path::Path>,
20 ) -> Result<Self, InstalledMetadataError> {
21 Self::read_installed_with_options(package_dir, &crate::file::ReadOptions::default())
22 }
23
24 pub fn read_installed_with_options(
27 package_dir: impl AsRef<std::path::Path>,
28 options: &crate::file::ReadOptions,
29 ) -> Result<Self, InstalledMetadataError> {
30 let (path, object) = super::read_installed_object(package_dir, "package.rds", options)?;
31 Self::from_object(&object).map_err(|source| InstalledMetadataError::View { path, source })
32 }
33
34 pub fn from_object(object: &RObject) -> Result<Self, ViewError> {
36 let items = expect_list(object, "PackageMeta", None)?;
37 require_class(object, "PackageMeta", None, "packageDescription2")?;
38 let names = named_values(object, "PackageMeta", None)?;
39 if names.len() != items.len() {
40 return Err(unexpected_length(
41 "PackageMeta",
42 None,
43 items.len().to_string(),
44 names.len(),
45 ));
46 }
47
48 let mut positions = BTreeMap::new();
49 for (index, name) in names.iter().enumerate() {
50 let name = decode_required(name, &format!("PackageMeta[{index}]"), None)?;
51 if positions.insert(name.clone(), index).is_some() {
52 return Err(duplicate(&format!("PackageMeta.{name}"), Some(name)));
53 }
54 }
55
56 let description_index = positions
57 .get("DESCRIPTION")
58 .copied()
59 .ok_or_else(|| missing("PackageMeta.DESCRIPTION", Some("DESCRIPTION".to_owned())))?;
60 let description = parse_description(
61 &items[description_index],
62 "PackageMeta.DESCRIPTION",
63 Some("DESCRIPTION"),
64 )?;
65 let built = positions
66 .get("Built")
67 .copied()
68 .map(|index| parse_built(&items[index], "PackageMeta.Built"))
69 .transpose()?;
70
71 Ok(Self { description, built })
72 }
73
74 pub fn built(&self) -> Option<&Built> {
76 self.built.as_ref()
77 }
78
79 pub fn description(&self) -> &BTreeMap<String, Option<String>> {
81 &self.description
82 }
83
84 pub fn description_field(&self, name: &str) -> Option<Option<&str>> {
86 self.description.get(name).map(|value| value.as_deref())
87 }
88}
89
90impl TryFrom<&RObject> for PackageMeta {
91 type Error = ViewError;
92
93 fn try_from(value: &RObject) -> Result<Self, Self::Error> {
94 Self::from_object(value)
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Built {
108 r_version: PackageVersion,
109 platform: Option<String>,
110 date: Option<String>,
111 os_type: Option<String>,
112}
113
114impl Built {
115 pub fn r_version(&self) -> &PackageVersion {
117 &self.r_version
118 }
119
120 pub fn platform(&self) -> Option<&str> {
122 self.platform.as_deref()
123 }
124
125 pub fn date(&self) -> Option<&str> {
127 self.date.as_deref()
128 }
129
130 pub fn os_type(&self) -> Option<&str> {
132 self.os_type.as_deref()
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct PackageVersion {
139 pub(super) components: Vec<u32>,
140}
141
142impl PackageVersion {
143 pub fn components(&self) -> &[u32] {
145 &self.components
146 }
147}
148
149impl fmt::Display for PackageVersion {
150 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151 let mut components = self.components.iter();
152 if let Some(first) = components.next() {
153 write!(formatter, "{first}")?;
154 for component in components {
155 write!(formatter, ".{component}")?;
156 }
157 }
158 Ok(())
159 }
160}
161
162fn parse_description(
163 object: &RObject,
164 path: &str,
165 field: Option<&str>,
166) -> Result<BTreeMap<String, Option<String>>, ViewError> {
167 let values = match &object.value() {
168 RValue::Character(values) => values,
169 value => {
170 return Err(unexpected_type(
171 path,
172 field,
173 "character vector",
174 value.kind_name(),
175 ));
176 }
177 };
178 let names = named_values(object, path, field)?;
179 if names.len() != values.len() {
180 return Err(unexpected_length(
181 path,
182 field,
183 values.len().to_string(),
184 names.len(),
185 ));
186 }
187 let mut description = BTreeMap::new();
188 for (index, (name, value)) in names.iter().zip(values).enumerate() {
189 let name = decode_required(name, &format!("{path}[{index}]"), field)?;
190 if description.contains_key(&name) {
191 return Err(duplicate(&format!("{path}[\"{name}\"]"), Some(name)));
192 }
193 let value = decode_optional(value, &format!("{path}[\"{name}\"]"), Some(&name))?;
194 description.insert(name, value);
195 }
196 Ok(description)
197}
198
199fn parse_built(object: &RObject, path: &str) -> Result<Built, ViewError> {
200 let items = expect_list(object, path, Some("Built"))?;
201 let names = named_values(object, path, Some("Built"))?;
202 if names.len() != items.len() {
203 return Err(unexpected_length(
204 path,
205 Some("Built"),
206 items.len().to_string(),
207 names.len(),
208 ));
209 }
210 let mut positions = BTreeMap::new();
211 for (index, name) in names.iter().enumerate() {
212 let name = decode_required(name, &format!("{path}[{index}]"), Some("Built"))?;
213 if positions.insert(name.clone(), index).is_some() {
214 return Err(duplicate(&format!("{path}.{name}"), Some(name)));
215 }
216 }
217 let r_index = positions
218 .get("R")
219 .copied()
220 .ok_or_else(|| missing(format!("{path}.R"), Some("R".to_owned())))?;
221 let r_version = parse_version(&items[r_index], &format!("{path}.R"))?;
222 let platform = optional_built_string(&positions, items, "Platform", path)?;
223 let date = optional_built_string(&positions, items, "Date", path)?;
224 let os_type = optional_built_string(&positions, items, "OStype", path)?;
225 Ok(Built {
226 r_version,
227 platform,
228 date,
229 os_type,
230 })
231}
232
233fn parse_version(object: &RObject, path: &str) -> Result<PackageVersion, ViewError> {
234 let valid_class = match object.attributes().get("class") {
235 Some(attribute) => match &attribute.value() {
236 RValue::Character(values) => {
237 let mut has_package = false;
238 let mut has_numeric = false;
239 for value in values {
240 match value.as_str() {
241 Some(Ok(value)) if value == "package_version" => has_package = true,
242 Some(Ok(value)) if value == "numeric_version" => has_numeric = true,
243 Some(Err(_)) => {
244 return Err(ViewError::InvalidStringEncoding {
245 path: format!("{path}.class"),
246 field: Some("R".to_owned()),
247 row: None,
248 column: None,
249 });
250 }
251 _ => {}
252 }
253 }
254 has_package && has_numeric
255 }
256 _ => false,
257 },
258 None => false,
259 };
260 if !valid_class {
261 return Err(invalid_version(
262 path,
263 "missing package_version/numeric_version class",
264 ));
265 }
266 let RValue::List(values) = &object.value() else {
267 return Err(invalid_version(path, "expected a length-one list"));
268 };
269 if values.len() != 1 {
270 return Err(invalid_version(path, "expected a length-one list"));
271 }
272 let RValue::Integer(components) = &values[0].value() else {
273 return Err(invalid_version(path, "expected an integer vector"));
274 };
275 if components.is_empty() {
276 return Err(invalid_version(
277 path,
278 "version components must not be empty",
279 ));
280 }
281 let mut owned = Vec::with_capacity(components.len());
282 for (index, component) in components.iter().enumerate() {
283 let Some(component) = component else {
284 return Err(invalid_version(
285 &format!("{path}[0][{index}]"),
286 "component is NA",
287 ));
288 };
289 if *component < 0 {
290 return Err(invalid_version(
291 &format!("{path}[0][{index}]"),
292 "component is negative",
293 ));
294 }
295 owned.push(*component as u32);
296 }
297 Ok(PackageVersion { components: owned })
298}
299
300fn optional_built_string(
301 positions: &BTreeMap<String, usize>,
302 items: &[RObject],
303 name: &str,
304 path: &str,
305) -> Result<Option<String>, ViewError> {
306 match positions.get(name) {
307 Some(index) => {
308 decode_character_scalar(&items[*index], &format!("{path}.{name}"), Some(name))
309 }
310 None => Ok(None),
311 }
312}
313
314fn require_class(
315 object: &RObject,
316 path: &str,
317 field: Option<&str>,
318 expected: &str,
319) -> Result<(), ViewError> {
320 let Some(attribute) = object.attributes().get("class") else {
321 return Err(missing(format!("{path}.class"), field.map(str::to_owned)));
322 };
323 let RValue::Character(values) = &attribute.value() else {
324 return Err(unexpected_type(
325 &format!("{path}.class"),
326 field,
327 "character vector",
328 attribute.value().kind_name(),
329 ));
330 };
331 for value in values {
332 match value.as_str() {
333 Some(Ok(value)) if value == expected => return Ok(()),
334 Some(Err(_)) => {
335 return Err(ViewError::InvalidStringEncoding {
336 path: format!("{path}.class"),
337 field: field.map(str::to_owned),
338 row: None,
339 column: None,
340 });
341 }
342 _ => {}
343 }
344 }
345 Err(unexpected_type(
346 path,
347 field,
348 "expected class",
349 "different class",
350 ))
351}
352
353fn decode_character_scalar(
354 object: &RObject,
355 path: &str,
356 field: Option<&str>,
357) -> Result<Option<String>, ViewError> {
358 let RValue::Character(values) = &object.value() else {
359 return Err(unexpected_type(
360 path,
361 field,
362 "character scalar",
363 object.value().kind_name(),
364 ));
365 };
366 if values.len() != 1 {
367 return Err(unexpected_length(path, field, "1".to_owned(), values.len()));
368 }
369 decode_optional(&values[0], path, field)
370}
371
372fn invalid_version(path: &str, reason: &str) -> ViewError {
373 ViewError::InvalidPackageVersion {
374 path: path.to_owned(),
375 field: Some("R".to_owned()),
376 reason: reason.to_owned(),
377 }
378}