Skip to main content

rd_rds/package/
mod.rs

1//! Typed access to installed-package metadata, installed code databases, and
2//! CRAN-like repository indexes.
3//!
4//! [`PackageMeta`] covers the `packageDescription2` shape written by R for an
5//! installed package. It is intentionally a typed reader rather than a
6//! general R object model: values are validated and copied into owned Rust
7//! data during construction. The raw [`crate::RObject`] remains available for
8//! metadata shapes not covered here.
9//!
10//! A missing package field is represented by an outer [`Option`], while an R
11//! `NA` character value is represented by an inner [`Option`]. Thus
12//! [`PackageMeta::description_field`] can distinguish an absent field from a
13//! present field whose value is `NA`. [`Built`] is the deliberate exception:
14//! its optional character accessors collapse both cases to `None`, because an
15//! absent and an `NA` build field carry the same meaning to consumers.
16//!
17//! [`PackagesMatrix`] covers CRAN-like `PACKAGES.rds` character matrices. It
18//! validates and owns the matrix data, absorbing R's column-major layout.
19//! Row/column lookup uses an outer `Option` for a missing column or row and an
20//! inner `Option` for an R `NA` cell.
21//!
22//! [`NamespaceMetadata`] provides a separate owned view of static declarations
23//! from `Meta/nsInfo.rds`. It does not represent runtime namespace state or
24//! stored lazy-load bindings.
25//!
26//! [`PackageMeta::read_installed`] and [`NamespaceMetadata::read_installed`]
27//! are thin convenience readers for the canonical `Meta/package.rds` and
28//! `Meta/nsInfo.rds` paths below a caller-supplied installed package
29//! directory. They do not discover packages or apply runtime policy. Their
30//! `_with_options` variants expose the bounded [`crate::file::ReadOptions`]
31//! used by the standalone file layer.
32
33use std::path::{Path, PathBuf};
34
35use thiserror::Error;
36
37use crate::{RObject, RStr, RValue};
38
39mod namespace;
40
41#[cfg(feature = "lazyload")]
42mod installed_code;
43
44#[cfg(feature = "lazyload")]
45pub use crate::inspection::{
46    BodyValidation, DefaultPresence, FailureCause, FailurePhase, Formal, FormalsInspection,
47    FormalsNotApplicable, FormalsUnavailable, FunctionFormals, InspectionExtent, PrefixFailure,
48    StoredKind, StoredObjectInspection,
49};
50
51#[cfg(feature = "lazyload")]
52pub use installed_code::{
53    CodeDbGeneration, CodeDbProvenance, InstalledCodeDb, InstalledCodeError, InstalledCodeOptions,
54    StoredBinding,
55};
56
57pub use namespace::{
58    ImportedName, MetadataField, NamespaceExport, NamespaceImport, NamespaceMetadata, S3MethodName,
59    S3Registration,
60};
61
62/// Errors reading a typed view from an installed-package metadata artifact.
63///
64/// The file layer and typed view layer remain separate: callers can inspect
65/// whether a failure came from bounded file reading or from validating the
66/// decoded metadata object. The artifact path is retained for both variants,
67/// including read failures that do not carry a path themselves (for example,
68/// a decompression or size-limit error).
69#[derive(Debug, Error)]
70#[non_exhaustive]
71pub enum InstalledMetadataError {
72    /// Reading the canonical installed metadata artifact failed.
73    #[error("failed to read installed metadata at {path}: {source}")]
74    Read {
75        path: PathBuf,
76        #[source]
77        source: crate::file::ReadError,
78    },
79    /// The artifact was read, but its decoded object did not match the typed
80    /// metadata view's supported schema.
81    #[error("invalid installed metadata at {path}: {source}")]
82    View {
83        path: PathBuf,
84        #[source]
85        source: ViewError,
86    },
87}
88
89impl InstalledMetadataError {
90    /// Returns the canonical artifact path selected for the read.
91    #[must_use]
92    pub fn path(&self) -> &Path {
93        match self {
94            Self::Read { path, .. } | Self::View { path, .. } => path,
95        }
96    }
97}
98
99/// A construction error from the typed installed-package metadata view.
100#[derive(Debug, Error, Clone, PartialEq, Eq)]
101#[non_exhaustive]
102pub enum ViewError {
103    #[error("missing value at {path}")]
104    Missing { path: String, field: Option<String> },
105    #[error("unexpected type at {path}: expected {expected}, got {actual}")]
106    UnexpectedType {
107        path: String,
108        field: Option<String>,
109        expected: &'static str,
110        actual: &'static str,
111    },
112    #[error("unexpected length at {path}: expected {expected}, got {actual}")]
113    UnexpectedLength {
114        path: String,
115        field: Option<String>,
116        expected: String,
117        actual: usize,
118    },
119    #[error("duplicate name at {path}")]
120    DuplicateName { path: String, field: Option<String> },
121    #[error("invalid string encoding at {path}")]
122    InvalidStringEncoding {
123        path: String,
124        field: Option<String>,
125        row: Option<usize>,
126        column: Option<String>,
127    },
128    #[error("invalid dimensions at {path}: {reason}")]
129    InvalidDimensions {
130        path: String,
131        field: Option<String>,
132        reason: String,
133    },
134    #[error("invalid package version at {path}: {reason}")]
135    InvalidPackageVersion {
136        path: String,
137        field: Option<String>,
138        reason: String,
139    },
140}
141
142impl ViewError {
143    /// Returns the logical location of the invalid value.
144    pub fn path(&self) -> String {
145        match self {
146            Self::Missing { path, .. }
147            | Self::UnexpectedType { path, .. }
148            | Self::UnexpectedLength { path, .. }
149            | Self::DuplicateName { path, .. }
150            | Self::InvalidStringEncoding { path, .. }
151            | Self::InvalidDimensions { path, .. }
152            | Self::InvalidPackageVersion { path, .. } => path.clone(),
153        }
154    }
155
156    /// Returns the metadata field associated with the error, when there is one.
157    pub fn field(&self) -> Option<&str> {
158        match self {
159            Self::Missing { field, .. }
160            | Self::UnexpectedType { field, .. }
161            | Self::UnexpectedLength { field, .. }
162            | Self::DuplicateName { field, .. }
163            | Self::InvalidStringEncoding { field, .. }
164            | Self::InvalidDimensions { field, .. }
165            | Self::InvalidPackageVersion { field, .. } => field.as_deref(),
166        }
167    }
168
169    /// Returns row context when the error was caused by a matrix cell.
170    pub fn row(&self) -> Option<usize> {
171        match self {
172            Self::InvalidStringEncoding { row, .. } => *row,
173            _ => None,
174        }
175    }
176
177    /// Returns column-name context when the error was caused by a matrix cell.
178    pub fn column(&self) -> Option<&str> {
179        match self {
180            Self::InvalidStringEncoding { column, .. } => column.as_deref(),
181            _ => None,
182        }
183    }
184}
185
186mod meta;
187mod packages;
188
189pub use meta::{Built, PackageMeta, PackageVersion};
190pub use packages::{PackagesColumn, PackagesMatrix, PackagesRow};
191
192fn read_installed_object(
193    package_dir: impl AsRef<Path>,
194    artifact: &str,
195    options: &crate::file::ReadOptions,
196) -> Result<(PathBuf, RObject), InstalledMetadataError> {
197    let path = package_dir.as_ref().join("Meta").join(artifact);
198    let object = crate::file::read_with_options(&path, options).map_err(|source| {
199        InstalledMetadataError::Read {
200            path: path.clone(),
201            source,
202        }
203    })?;
204    Ok((path, object))
205}
206
207fn expect_list<'a>(
208    object: &'a RObject,
209    path: &str,
210    field: Option<&str>,
211) -> Result<&'a [RObject], ViewError> {
212    match &object.value() {
213        RValue::List(values) => Ok(values),
214        value => Err(unexpected_type(path, field, "list", value.kind_name())),
215    }
216}
217
218fn named_values<'a>(
219    object: &'a RObject,
220    path: &str,
221    field: Option<&str>,
222) -> Result<&'a [RStr], ViewError> {
223    let Some(attribute) = object.attributes().get("names") else {
224        return Err(missing(format!("{path}.names"), field.map(str::to_owned)));
225    };
226    match &attribute.value() {
227        RValue::Character(values) => Ok(values),
228        value => Err(unexpected_type(
229            &format!("{path}.names"),
230            field,
231            "character vector",
232            value.kind_name(),
233        )),
234    }
235}
236
237fn decode_optional(
238    value: &RStr,
239    path: &str,
240    field: Option<&str>,
241) -> Result<Option<String>, ViewError> {
242    match value.as_str() {
243        None => Ok(None),
244        Some(Ok(value)) => Ok(Some(value.into_owned())),
245        Some(Err(_)) => Err(ViewError::InvalidStringEncoding {
246            path: path.to_owned(),
247            field: field.map(str::to_owned),
248            row: None,
249            column: None,
250        }),
251    }
252}
253
254fn invalid_dimensions(path: &str, reason: &str) -> ViewError {
255    ViewError::InvalidDimensions {
256        path: path.to_owned(),
257        field: None,
258        reason: reason.to_owned(),
259    }
260}
261
262fn decode_required(value: &RStr, path: &str, field: Option<&str>) -> Result<String, ViewError> {
263    match value.as_str() {
264        None => Err(unexpected_type(path, field, "non-NA string", "NA")),
265        Some(Ok(value)) => Ok(value.into_owned()),
266        Some(Err(_)) => Err(ViewError::InvalidStringEncoding {
267            path: path.to_owned(),
268            field: field.map(str::to_owned),
269            row: None,
270            column: None,
271        }),
272    }
273}
274
275fn missing(path: impl Into<String>, field: Option<String>) -> ViewError {
276    ViewError::Missing {
277        path: path.into(),
278        field,
279    }
280}
281
282fn unexpected_type(
283    path: &str,
284    field: Option<&str>,
285    expected: &'static str,
286    actual: &'static str,
287) -> ViewError {
288    ViewError::UnexpectedType {
289        path: path.to_owned(),
290        field: field.map(str::to_owned),
291        expected,
292        actual,
293    }
294}
295
296fn unexpected_length(
297    path: &str,
298    field: Option<&str>,
299    expected: String,
300    actual: usize,
301) -> ViewError {
302    ViewError::UnexpectedLength {
303        path: path.to_owned(),
304        field: field.map(str::to_owned),
305        expected,
306        actual,
307    }
308}
309
310fn duplicate(path: &str, field: Option<String>) -> ViewError {
311    ViewError::DuplicateName {
312        path: path.to_owned(),
313        field,
314    }
315}
316
317trait ValueKindName {
318    fn kind_name(&self) -> &'static str;
319}
320
321impl ValueKindName for RValue {
322    fn kind_name(&self) -> &'static str {
323        match self {
324            Self::Null => "null",
325            Self::Logical(_) => "logical vector",
326            Self::Integer(_) => "integer vector",
327            Self::Real(_) => "real vector",
328            Self::Character(_) => "character vector",
329            Self::List(_) => "list",
330            Self::Symbol(_) => "symbol",
331            Self::Persisted(_) => "persisted value",
332            Self::Environment(_) => "environment",
333        }
334    }
335}
336
337#[cfg(test)]
338mod tests;