docsearch 0.3.5

Resolve crate items to rustdoc URLs.
Documentation
#![allow(clippy::cast_possible_truncation)]

use std::collections::HashMap;

use winnow::{
    ascii::dec_uint,
    combinator::{
        cut_err, delimited, fail, fold_repeat, peek, preceded, separated0, separated_pair, success,
        terminated,
    },
    dispatch,
    error::StrContext,
    stream::AsChar,
    token::{any, none_of, take_while},
    PResult, Parser, Stateful,
};

use super::{v2::RawCrate, RawIndexData};
use crate::error::IndexV1Error as Error;

pub(super) fn load_raw(index: &str) -> Result<RawIndexData, Error> {
    let r = {
        let r = index
            .lines()
            .find_map(|line| line.strip_prefix("var R="))
            .and_then(|line| line.strip_suffix(';'))
            .ok_or(Error::MissingReference)?;
        serde_json::from_str::<Vec<String>>(r).map_err(Error::InvalidReferenceJson)?
    };

    let entries = index.lines().filter_map(|line| {
        line.strip_prefix("searchIndex[\"")
            .and_then(|line| line.strip_suffix(';'))
            .and_then(|line| line.split_once("\"]="))
    });

    let crates = entries
        .map(|(name, index)| {
            let json = match json.parse(Stateful {
                input: index,
                state: r.as_slice(),
            }) {
                Ok(json) => json,
                Err(err) => {
                    return Err(Error::InvalidIndexJavaScript(format!("Parse error: {err}")));
                }
            };
            let json = serde_json::Value::try_from(json).map_err(Error::InvalidIndexJavaScript)?;
            let data = serde_json::from_value::<RawCrate>(json)
                .map_err(Error::InvalidIndexJson)?
                .into();

            Ok((name.to_owned(), data))
        })
        .collect::<Result<_, _>>()?;

    Ok(RawIndexData { crates })
}

#[derive(Clone, Debug)]
enum JsJson {
    Null,
    Str(String),
    Num(usize),
    Array(Vec<JsJson>),
    Object(HashMap<String, JsJson>),
}

impl TryFrom<JsJson> for serde_json::Value {
    type Error = String;

    fn try_from(value: JsJson) -> Result<Self, Self::Error> {
        Ok(match value {
            JsJson::Null => serde_json::Value::Null,
            JsJson::Str(string) => serde_json::Value::String(string),
            JsJson::Num(num) => serde_json::Value::Number(num.into()),
            JsJson::Array(array) => serde_json::Value::Array(
                array
                    .into_iter()
                    .map(TryInto::try_into)
                    .collect::<Result<_, String>>()?,
            ),
            JsJson::Object(object) => serde_json::Value::Object(
                object
                    .into_iter()
                    .map(|(k, v)| Ok((k, v.try_into()?)))
                    .collect::<Result<_, String>>()?,
            ),
        })
    }
}

type Stream<'i> = Stateful<&'i str, &'i [String]>;

fn json(input: &mut Stream<'_>) -> PResult<JsJson> {
    delimited(ws, json_value, ws).parse_next(input)
}

fn json_value(input: &mut Stream<'_>) -> PResult<JsJson> {
    dispatch!(
        peek(any);
        'n' => null.value(JsJson::Null),
        '0'..='9' => number.map(JsJson::Num),
        '"' => string.map(JsJson::Str),
        '[' => array.map(JsJson::Array),
        '{' => object.map(JsJson::Object),
        'N' => "N".value(JsJson::Null),
        'E' => "E".map(|_| JsJson::Str(String::new())),
        'T' => "T".map(|_| JsJson::Str("t".to_owned())),
        'U' => "U".map(|_| JsJson::Str("u".to_owned())),
        'R' => reference,
        _ => fail,
    )
    .context(StrContext::Label("value"))
    .parse_next(input)
}

fn null<'i>(input: &mut Stream<'i>) -> PResult<&'i str> {
    "null".context(StrContext::Label("null")).parse_next(input)
}

fn number(input: &mut Stream<'_>) -> PResult<usize> {
    dec_uint
        .map(|v: u64| v as usize)
        .context(StrContext::Label("number"))
        .parse_next(input)
}

fn string(input: &mut Stream<'_>) -> PResult<String> {
    preceded(
        '\"',
        cut_err(terminated(
            fold_repeat(0.., character, String::new, |mut string, c| {
                string.push(c);
                string
            }),
            '\"',
        )),
    )
    .context(StrContext::Label("string"))
    .parse_next(input)
}

fn character(input: &mut Stream<'_>) -> PResult<char> {
    let c = none_of('\"').parse_next(input)?;
    if c == '\\' {
        dispatch!(
            any;
            '"' => success('"'),
            '\\' => success('\\'),
            '/'  => success('/'),
            'b' => success('\x08'),
            'f' => success('\x0C'),
            'n' => success('\n'),
            'r' => success('\r'),
            't' => success('\t'),
            'u' => unicode_escape,
            _ => fail,
        )
        .parse_next(input)
    } else {
        Ok(c)
    }
}

fn unicode_escape(input: &mut Stream<'_>) -> PResult<char> {
    take_while(4, AsChar::is_hex_digit)
        .map(|hex| char::from_u32(u32::from_str_radix(hex, 16).unwrap()).unwrap_or('\u{FFFD}'))
        .parse_next(input)
}

fn array(input: &mut Stream<'_>) -> PResult<Vec<JsJson>> {
    preceded(
        ('[', ws),
        cut_err(terminated(separated0(json_value, (ws, ',', ws)), (ws, ']'))),
    )
    .context(StrContext::Label("array"))
    .parse_next(input)
}

fn object(input: &mut Stream<'_>) -> PResult<HashMap<String, JsJson>> {
    preceded(
        ('{', ws),
        cut_err(terminated(separated0(key_value, (ws, ',', ws)), (ws, '}'))),
    )
    .context(StrContext::Label("object"))
    .parse_next(input)
}

fn key_value(input: &mut Stream<'_>) -> PResult<(String, JsJson)> {
    separated_pair(string, cut_err((ws, ':', ws)), json_value).parse_next(input)
}

fn reference(input: &mut Stream<'_>) -> PResult<JsJson> {
    preceded(
        "R[",
        cut_err(terminated(
            dec_uint.try_map(|v: u64| {
                input
                    .state
                    .get(v as usize)
                    .map(|r| JsJson::Str(r.clone()))
                    .ok_or(Error::MissingReference)
            }),
            ']',
        )),
    )
    .context(StrContext::Label("reference"))
    .parse_next(input)
}

fn ws<'a>(input: &mut Stream<'a>) -> PResult<&'a str> {
    take_while(0.., &[' ', '\t', '\r', '\n']).parse_next(input)
}