use std::{
collections::{BTreeMap, HashMap},
fmt,
};
use serde::{
de::{SeqAccess, Visitor},
Deserialize, Deserializer,
};
use serde_repr::Deserialize_repr;
use crate::error::{Error, Result};
#[cfg(feature = "index-v1")]
mod v1;
#[cfg(feature = "index-v2")]
mod v2;
#[cfg_attr(test, derive(Clone, Copy, Eq, PartialEq, serde::Serialize))]
enum Version {
#[cfg(feature = "index-v1")]
V1,
#[cfg(feature = "index-v2")]
V2,
V3,
}
impl Version {
fn detect(index: &str) -> Option<Self> {
#[cfg(feature = "index-v1")]
if index.starts_with(r#"var N=null,E="",T="t",U="u",searchIndex={};"#) {
return Some(Self::V1);
}
#[cfg(feature = "index-v2")]
if index.ends_with(r#"addSearchOptions(searchIndex);initSearch(searchIndex);"#) {
return Some(Self::V2);
}
if index.ends_with(r#"if (window.initSearch) {window.initSearch(searchIndex)};"#)
|| index.trim_end().ends_with(
r#"if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};"#,
)
{
Some(Self::V3)
} else {
None
}
}
}
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct IndexData {
crates: HashMap<String, CrateData>,
}
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct CrateData {
#[allow(dead_code)]
doc: String,
items: Vec<IndexItem>,
paths: Vec<(ItemType, String)>,
}
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct IndexItem {
ty: ItemType,
name: String,
path: String,
#[allow(dead_code)]
desc: String,
parent_idx: Option<usize>,
}
#[derive(Clone, Copy, Debug, Deserialize_repr)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
#[repr(u8)]
enum ItemType {
Module = 0,
ExternCrate = 1,
Import = 2,
Struct = 3,
Enum = 4,
Function = 5,
Typedef = 6,
Static = 7,
Trait = 8,
Impl = 9,
TyMethod = 10,
Method = 11,
StructField = 12,
Variant = 13,
Macro = 14,
Primitive = 15,
AssocType = 16,
Constant = 17,
AssocConst = 18,
Union = 19,
ForeignType = 20,
Keyword = 21,
OpaqueTy = 22,
ProcAttribute = 23,
ProcDerive = 24,
TraitAlias = 25,
}
impl ItemType {
const fn as_str(self) -> &'static str {
match self {
Self::Module => "mod",
Self::ExternCrate => "externcrate",
Self::Import => "import",
Self::Struct => "struct",
Self::Union => "union",
Self::Enum => "enum",
Self::Function => "fn",
Self::Typedef => "type",
Self::Static => "static",
Self::Trait => "trait",
Self::Impl => "impl",
Self::TyMethod => "tymethod",
Self::Method => "method",
Self::StructField => "structfield",
Self::Variant => "variant",
Self::Macro => "macro",
Self::Primitive => "primitive",
Self::AssocType => "associatedtype",
Self::Constant => "constant",
Self::AssocConst => "associatedconstant",
Self::ForeignType => "foreigntype",
Self::Keyword => "keyword",
Self::OpaqueTy => "opaque",
Self::ProcAttribute => "attr",
Self::ProcDerive => "derive",
Self::TraitAlias => "traitalias",
}
}
const fn from_raw(value: u8) -> Option<Self> {
Some(match value {
0 => Self::Module,
1 => Self::ExternCrate,
2 => Self::Import,
3 => Self::Struct,
4 => Self::Enum,
5 => Self::Function,
6 => Self::Typedef,
7 => Self::Static,
8 => Self::Trait,
9 => Self::Impl,
10 => Self::TyMethod,
11 => Self::Method,
12 => Self::StructField,
13 => Self::Variant,
14 => Self::Macro,
15 => Self::Primitive,
16 => Self::AssocType,
17 => Self::Constant,
18 => Self::AssocConst,
19 => Self::Union,
20 => Self::ForeignType,
21 => Self::Keyword,
22 => Self::OpaqueTy,
23 => Self::ProcAttribute,
24 => Self::ProcDerive,
25 => Self::TraitAlias,
_ => return None,
})
}
}
#[derive(Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct RawIndexData {
#[serde(flatten)]
crates: HashMap<String, RawCrateData>,
}
#[derive(Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
struct RawCrateData {
doc: String,
#[serde(deserialize_with = "t")]
t: Vec<ItemType>,
n: Vec<String>,
#[serde(deserialize_with = "q")]
q: BTreeMap<usize, String>,
d: Vec<String>,
i: Vec<usize>,
p: Vec<(ItemType, String)>,
}
pub fn load(index: &str) -> Result<HashMap<String, BTreeMap<String, String>>> {
let raw = match Version::detect(index) {
Some(Version::V3) => load_raw(index)?,
#[cfg(feature = "index-v2")]
Some(Version::V2) => v2::load_raw(index)?,
#[cfg(feature = "index-v1")]
Some(Version::V1) => v1::load_raw(index)?,
None => return Err(Error::UnsupportedIndexVersion),
};
Ok(generate_mapping(transform(raw)))
}
fn load_raw(index: &str) -> Result<RawIndexData> {
let json = {
let mut json = index
.lines()
.filter_map(|l| {
if l.starts_with('"') {
l.strip_suffix('\\')
} else {
None
}
})
.fold(String::from("{"), |mut json, l| {
json.push_str(l);
json
});
json.push('}');
json.replace("\\\\\"", "\\\"")
.replace(r"\'", "'")
.replace(r"\\", r"\")
};
serde_json::from_str(&json).map_err(Into::into)
}
fn transform(raw: RawIndexData) -> IndexData {
IndexData {
crates: raw
.crates
.into_iter()
.map(|(name, mut raw_data)| {
let length = raw_data.t.len();
let (items, _) = raw_data
.t
.into_iter()
.enumerate()
.zip(raw_data.n.into_iter())
.zip(raw_data.d.into_iter())
.zip(raw_data.i.into_iter())
.fold(
(Vec::with_capacity(length), String::new()),
|(mut items, path), ((((pos, t), n), d), i)| {
let path = raw_data.q.remove(&pos).unwrap_or(path);
items.push(IndexItem {
ty: t,
name: n,
path: path.clone(),
desc: d,
parent_idx: if i > 0 { Some(i - 1) } else { None },
});
(items, path)
},
);
(
name,
CrateData {
doc: raw_data.doc,
items,
paths: raw_data.p,
},
)
})
.collect(),
}
}
fn generate_mapping(data: IndexData) -> HashMap<String, BTreeMap<String, String>> {
data.crates
.into_iter()
.map(|(name, data)| (name, generate_crate_mapping(data)))
.collect()
}
fn generate_crate_mapping(data: CrateData) -> BTreeMap<String, String> {
let paths = data.paths;
data.items
.into_iter()
.map(|item| {
let full_path = if let Some(idx) = item.parent_idx {
format!("{}::{}::{}", item.path, paths[idx].1, item.name)
} else {
format!("{}::{}", item.path, item.name)
};
let url = if let Some(parent) = item.parent_idx.map(|i| &paths[i]) {
format!(
"{}/{}.{}.html#{}.{}",
item.path.replace("::", "/"),
parent.0.as_str(),
parent.1,
item.ty.as_str(),
item.name
)
} else {
format!(
"{}/{}.{}.html",
item.path.replace("::", "/"),
item.ty.as_str(),
item.name
)
};
(full_path, url)
})
.collect()
}
fn t<'de, D>(deserializer: D) -> Result<Vec<ItemType>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(VecItemTypeVisitor)
}
struct VecItemTypeVisitor;
impl<'de> Visitor<'de> for VecItemTypeVisitor {
type Value = Vec<ItemType>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("item types either as an array of IDs or a string of ASCII chars")
}
fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
v.bytes()
.map(|ascii| {
ascii
.is_ascii_uppercase()
.then(|| ItemType::from_raw(ascii - b'A'))
.flatten()
.ok_or_else(|| {
E::custom(format!("invalid ASCII character `{}`", ascii as char))
})
})
.collect()
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut list = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(element) = seq.next_element()? {
list.push(element);
}
Ok(list)
}
}
fn q<'de, D>(deserializer: D) -> Result<BTreeMap<usize, String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(VecPathVisitor)
}
struct VecPathVisitor;
impl<'de> Visitor<'de> for VecPathVisitor {
type Value = BTreeMap<usize, String>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("item types either as an array of IDs or a string of ASCII chars")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Value {
String(String),
Tuple((usize, String)),
}
let mut map = BTreeMap::new();
let mut position = 0;
while let Some(element) = seq.next_element::<Value>()? {
let (key, value) = match element {
Value::String(name) => {
if name.is_empty() {
position += 1;
continue;
}
(position, name)
}
Value::Tuple((position, name)) => (position, name),
};
map.insert(key, value);
position += 1;
}
Ok(map)
}
}
#[cfg(test)]
mod tests {
use std::fs;
use insta::glob;
use serde_test::Token;
use super::*;
#[test]
fn test_version_detect() {
glob!("fixtures/*.js", |path| {
let input = fs::read_to_string(path).unwrap();
let data = Version::detect(&input);
insta::assert_yaml_snapshot!(data);
});
}
#[allow(clippy::bind_instead_of_map)]
#[test]
fn test_load_raw() {
glob!("fixtures/*.js", |path| {
let input = fs::read_to_string(path).unwrap();
let data = Version::detect(&input).and_then(|v| match v {
#[cfg(feature = "index-v1")]
Version::V1 => Some(v1::load_raw(&input).unwrap()),
#[cfg(feature = "index-v2")]
Version::V2 => Some(v2::load_raw(&input).unwrap()),
Version::V3 => Some(load_raw(&input).unwrap()),
});
insta::assert_yaml_snapshot!(data);
});
}
#[allow(clippy::bind_instead_of_map)]
#[test]
fn test_transform() {
glob!("fixtures/*.js", |path| {
let input = fs::read_to_string(path).unwrap();
let data = Version::detect(&input)
.and_then(|v| match v {
#[cfg(feature = "index-v1")]
Version::V1 => Some(v1::load_raw(&input).unwrap()),
#[cfg(feature = "index-v2")]
Version::V2 => Some(v2::load_raw(&input).unwrap()),
Version::V3 => Some(load_raw(&input).unwrap()),
})
.map(transform);
insta::assert_yaml_snapshot!(data);
});
}
#[allow(clippy::bind_instead_of_map)]
#[test]
fn test_generate_mapping() {
glob!("fixtures/*.js", |path| {
let input = fs::read_to_string(path).unwrap();
let data = Version::detect(&input)
.and_then(|v| match v {
#[cfg(feature = "index-v1")]
Version::V1 => Some(v1::load_raw(&input).unwrap()),
#[cfg(feature = "index-v2")]
Version::V2 => Some(v2::load_raw(&input).unwrap()),
Version::V3 => Some(load_raw(&input).unwrap()),
})
.map(transform)
.map(generate_mapping);
insta::assert_yaml_snapshot!(data);
});
}
#[test]
fn test_t() {
#[derive(Debug, PartialEq, Deserialize)]
struct Wrapper {
#[serde(deserialize_with = "t")]
value: Vec<ItemType>,
}
let wrapper = Wrapper {
value: vec![ItemType::Module],
};
serde_test::assert_de_tokens(
&wrapper,
&[
Token::Struct {
name: "Wrapper",
len: 1,
},
Token::Str("value"),
Token::Str("A"),
Token::StructEnd,
],
);
serde_test::assert_de_tokens(
&wrapper,
&[
Token::Struct {
name: "Wrapper",
len: 1,
},
Token::Str("value"),
Token::Seq { len: Some(1) },
Token::I64(0),
Token::SeqEnd,
Token::StructEnd,
],
);
}
#[test]
fn test_q() {
#[derive(Debug, PartialEq, Deserialize)]
struct Wrapper {
#[serde(deserialize_with = "q")]
value: BTreeMap<usize, String>,
}
let wrapper = Wrapper {
value: [(0, "test".to_owned()), (2, "test::two".to_owned())].into(),
};
serde_test::assert_de_tokens(
&wrapper,
&[
Token::Struct {
name: "Wrapper",
len: 1,
},
Token::Str("value"),
Token::Seq { len: Some(2) },
Token::Seq { len: Some(2) },
Token::I64(0),
Token::Str("test"),
Token::SeqEnd,
Token::Seq { len: Some(2) },
Token::I64(2),
Token::Str("test::two"),
Token::SeqEnd,
Token::SeqEnd,
Token::StructEnd,
],
);
serde_test::assert_de_tokens(
&wrapper,
&[
Token::Struct {
name: "Wrapper",
len: 1,
},
Token::Str("value"),
Token::Seq { len: Some(3) },
Token::Str("test"),
Token::Str(""),
Token::Str("test::two"),
Token::SeqEnd,
Token::StructEnd,
],
);
}
}