use std::{
collections::HashMap,
fs::File,
io::{Read, Seek, SeekFrom},
path::{Path, PathBuf},
sync::OnceLock,
};
use rd_rds::RObject;
use crate::{
Error,
index::{Index, Record},
rds::{decode_rdb_record, read_rds_file},
util::rstr_to_string,
};
#[derive(Debug)]
pub struct PackageHelpDb {
package: String,
pkg_dir: PathBuf,
help_dir: PathBuf,
rdb_path: PathBuf,
index: Index,
alias_index: OnceLock<AliasIndex>,
}
#[derive(Debug)]
struct AliasIndex {
entries: Vec<(String, String)>,
lookup: HashMap<String, usize>,
}
impl AliasIndex {
fn from_root(root: &RObject) -> Result<Self, Error> {
let rd_rds::RValue::Character(values) = &root.value() else {
return Err(Error::MalformedIndex(
"aliases.rds root is not a character vector".into(),
));
};
let names = root.names().ok_or_else(|| {
Error::MalformedIndex("aliases.rds is missing a names attribute".into())
})?;
if names.len() != values.len() {
return Err(Error::MalformedIndex(format!(
"aliases.rds has {} names but {} values",
names.len(),
values.len()
)));
}
let entries = names
.iter()
.zip(values.iter())
.map(|(alias, topic)| Ok((rstr_to_string(alias)?, rstr_to_string(topic)?)))
.collect::<Result<Vec<(String, String)>, Error>>()?;
let mut lookup = HashMap::with_capacity(entries.len());
for (position, (alias, _)) in entries.iter().enumerate() {
lookup.insert(alias.clone(), position);
}
Ok(Self { entries, lookup })
}
}
impl PackageHelpDb {
pub fn open(pkg_dir: impl AsRef<Path>) -> Result<Self, Error> {
let pkg_dir = pkg_dir.as_ref();
let package = pkg_dir
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
Error::MalformedIndex(format!(
"cannot determine a package name from directory {}",
pkg_dir.display()
))
})?
.to_string();
let help_dir = pkg_dir.join("help");
let rdx_path = help_dir.join(format!("{package}.rdx"));
let rdb_path = help_dir.join(format!("{package}.rdb"));
let rdx_root = read_rds_file(&rdx_path)?;
let index = Index::from_rdx(&rdx_root)?;
Ok(Self {
package,
pkg_dir: pkg_dir.to_path_buf(),
help_dir,
rdb_path,
index,
alias_index: OnceLock::new(),
})
}
pub fn package(&self) -> &str {
&self.package
}
pub fn topics(&self) -> impl Iterator<Item = &str> {
self.index.topics()
}
pub fn reference_keys(&self) -> impl Iterator<Item = &str> {
self.index.reference_keys()
}
pub fn raw_topic(&self, topic: &str) -> Result<RObject, Error> {
let record = self
.index
.variable(topic)
.ok_or_else(|| Error::UnknownTopic {
topic: topic.to_string(),
})?;
self.fetch_record(record)
}
pub fn reference(&self, key: &str) -> Result<RObject, Error> {
let record = self
.index
.reference(key)
.ok_or_else(|| Error::UnknownReference {
key: key.to_string(),
})?;
self.fetch_record(record)
}
pub fn aliases(&self) -> Result<Vec<(String, String)>, Error> {
Ok(self.alias_index()?.entries.clone())
}
pub fn resolve_alias(&self, alias: &str) -> Result<Option<&str>, Error> {
let index = self.alias_index()?;
Ok(index
.lookup
.get(alias)
.map(|&position| index.entries[position].1.as_str()))
}
fn alias_index(&self) -> Result<&AliasIndex, Error> {
if let Some(index) = self.alias_index.get() {
return Ok(index);
}
let path = self.help_dir.join("aliases.rds");
let root = read_rds_file(&path)?;
let index = AliasIndex::from_root(&root)?;
let _ = self.alias_index.set(index);
Ok(self
.alias_index
.get()
.expect("alias_index was just set above"))
}
pub fn search_index(&self) -> Result<RObject, Error> {
let path = self.pkg_dir.join("Meta").join("hsearch.rds");
read_rds_file(&path)
}
fn fetch_record(&self, record: Record) -> Result<RObject, Error> {
let mut file = File::open(&self.rdb_path).map_err(|err| Error::io(&self.rdb_path, err))?;
file.seek(SeekFrom::Start(record.offset as u64))
.map_err(|err| Error::io(&self.rdb_path, err))?;
let mut buf = vec![0u8; record.length];
file.read_exact(&mut buf)
.map_err(|err| Error::io(&self.rdb_path, err))?;
decode_rdb_record(&buf)
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
fn dup_aliases_root() -> RObject {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/data/aliases_vector_dup_v3.rds");
read_rds_file(&path).unwrap_or_else(|err| panic!("read {}: {err}", path.display()))
}
#[test]
fn alias_index_resolves_duplicates_last_wins() {
let root = dup_aliases_root();
let index = AliasIndex::from_root(&root).expect("AliasIndex::from_root");
assert_eq!(
index.entries,
vec![
("shared".to_string(), "first-topic".to_string()),
("unique".to_string(), "unique-topic".to_string()),
("shared".to_string(), "second-topic".to_string()),
]
);
let shared_position = *index.lookup.get("shared").expect("'shared' in lookup");
assert_eq!(index.entries[shared_position].1, "second-topic");
let unique_position = *index.lookup.get("unique").expect("'unique' in lookup");
assert_eq!(index.entries[unique_position].1, "unique-topic");
}
}