tiny_v2 0.3.0

Parser for Fabric's Tiny v2 files
Documentation
pub mod error;

use crate::error::TinyV2Error;

/// # Example Header
/// ```tiny
/// tiny	2	0	official	intermediary	named
/// 	someProperty	someValue
/// 	anotherProperty
/// ```
#[derive(Debug, Default)]
pub struct Header<'a> {
    pub major_version: u32,
    pub minor_version: u32,
    pub namespace_a: &'a str,
    pub namespace_b: &'a str,
    pub namespaces: Vec<&'a str>,
    pub properties: Vec<Property<'a>>,
}

#[derive(Debug, Default)]
pub struct Property<'a> {
    pub key: &'a str,
    pub value: Option<&'a str>,
}

#[derive(Debug, Default)]
pub struct Content<'a> {
    pub class_section: Vec<ClassSection<'a>>,
}

#[derive(Debug, Default)]
pub struct ClassSection<'a> {
    pub class_name_a: &'a str,
    pub class_name_b: Option<&'a str>,
    pub extra_ns_class_names: Vec<&'a str>,
    pub class_sub_sections: Vec<ClassSubSection<'a>>,
}

#[derive(Debug)]
pub enum ClassSubSection<'a> {
    Comment(&'a str),
    Field {
        field_desc_a: &'a str,
        field_name_a: &'a str,
        field_name_b: Option<&'a str>,
        extra_ns_field_names: Vec<&'a str>,
        field_sub_sections: Vec<&'a str>,
    },
    Method {
        method_desc_a: &'a str,
        method_name_a: &'a str,
        method_name_b: Option<&'a str>,
        extra_ns_method_names: Vec<&'a str>,
        method_sub_sections: Vec<MethodSubSection<'a>>,
    },
}

/// # Example
/// p	1		param_0	x
#[derive(Debug)]
pub enum MethodSubSection<'a> {
    Comment(&'a str),
    Parameter {
        lv_index: u32,
        var_name_a: Option<&'a str>,
        var_name_b: Option<&'a str>,
        extra_ns_var_names: Vec<&'a str>,
        method_parameter_sub_sections: Vec<&'a str>,
    },
    Variable {
        lv_index: u32,
        lv_start_offset: u32,
        optional_lvt_index: i32,
        var_name_a: Option<&'a str>,
        var_name_b: Option<&'a str>,
        extra_ns_var_names: Vec<&'a str>,
        method_variable_sub_sections: Vec<&'a str>,
    },
}

/// Internal helper struct for storing sub sections
#[derive(Default)]
struct SectionBuilder<'a> {
    class_section: Vec<ClassSection<'a>>,
    class_sub_sections: Vec<ClassSubSection<'a>>,
    method_member_sub_sections: Vec<&'a str>,
    method_sub_sections: Vec<MethodSubSection<'a>>,
    properties: Vec<Property<'a>>,
}

/// Entire parsed tiny file
#[derive(Debug, Default)]
pub struct File<'a> {
    pub header: Header<'a>,
    pub content: Content<'a>,
}

impl<'a> File<'a> {
    fn from_str(s: &'a str) -> Result<Self, TinyV2Error> {
        let mut sections = SectionBuilder::default();
        let mut lines = s.lines();
        while let Some(line) = lines.next_back() {
            let leading_tabs = line.chars().take_while(|c| *c == '\t').count();
            let mut words = line.trim_ascii().split('\t');
            let identifier = words
                .next()
                .filter(|s| !s.is_empty())
                .ok_or(TinyV2Error::new("Line empty"))?;
            match (leading_tabs, identifier) {
                // header
                (0, "tiny") => {
                    let major_version = words
                        .next()
                        .ok_or(TinyV2Error::new("Major version not found"))?
                        .parse()
                        .map_err(|_| {
                            TinyV2Error::new("Major version must be a non negative integer")
                        })?;
                    let minor_version = words
                        .next()
                        .ok_or(TinyV2Error::new("Minor version not found"))?
                        .parse()
                        .map_err(|_| {
                            TinyV2Error::new("Minor version must be a non negative integer")
                        })?;
                    let namespace_a = words
                        .next()
                        .ok_or(TinyV2Error::new("Namespace a not found"))?;
                    let namespace_b = words
                        .next()
                        .ok_or(TinyV2Error::new("Namespace b not found"))?;
                    let namespaces: Vec<&'a str> = words.collect();
                    let properties = sections.properties;
                    return Ok(Self {
                        content: Content {
                            class_section: sections.class_section,
                        },
                        header: Header {
                            major_version,
                            minor_version,
                            namespace_a,
                            namespace_b,
                            namespaces,
                            properties,
                        },
                    });
                }
                // class-section
                (0, "c") => {
                    let class_name_a = words
                        .next()
                        .filter(|s| !s.is_empty())
                        .ok_or(TinyV2Error::new("Class name a not found"))?;
                    let class_name_b = words.next().filter(|s| !s.is_empty());
                    let extra_ns_class_names = words.collect();
                    let class_sub_sections = std::mem::take(&mut sections.class_sub_sections);
                    sections.class_section.push(ClassSection {
                        class_name_a,
                        class_name_b,
                        extra_ns_class_names,
                        class_sub_sections,
                    });
                }
                // class-comment-section
                (1, "c") => sections
                    .class_sub_sections
                    .push(ClassSubSection::Comment(words.next().unwrap_or_default())),
                // member-comment-section
                (2, "c") => sections
                    .method_sub_sections
                    .push(MethodSubSection::Comment(words.next().unwrap_or_default())),
                // var-comment-section
                (3, "c") => sections
                    .method_member_sub_sections
                    .push(words.next().unwrap_or_default()),
                // field-section
                (1, "f") => {
                    let field_desc_a = words
                        .next()
                        .filter(|s| !s.is_empty())
                        .ok_or(TinyV2Error::new("Field desc a not found"))?;
                    let field_name_a = words
                        .next()
                        .filter(|s| !s.is_empty())
                        .ok_or(TinyV2Error::new("Field name a not found"))?;
                    let field_name_b = words.next().filter(|s| !s.is_empty());
                    let extra_ns_field_names = words.collect();
                    let field_sub_sections = std::mem::take(&mut sections.method_sub_sections);
                    let field_sub_sections = field_sub_sections
                        .into_iter()
                        .filter_map(|mss| match mss {
                            MethodSubSection::Comment(c) => Some(c),
                            _ => None,
                        })
                        .collect();
                    sections.class_sub_sections.push(ClassSubSection::Field {
                        field_desc_a,
                        field_name_a,
                        field_name_b,
                        extra_ns_field_names,
                        field_sub_sections,
                    });
                }
                // method-section
                (1, "m") => {
                    let method_desc_a = words
                        .next()
                        .filter(|s| !s.is_empty())
                        .ok_or(TinyV2Error::new("Method desc a not found"))?;
                    let method_name_a = words
                        .next()
                        .filter(|s| !s.is_empty())
                        .ok_or(TinyV2Error::new("Method name a not found"))?;
                    let method_name_b = words.next().filter(|s| !s.is_empty());
                    let extra_ns_method_names = words.collect();
                    let method_sub_sections = std::mem::take(&mut sections.method_sub_sections);
                    sections.class_sub_sections.push(ClassSubSection::Method {
                        method_desc_a,
                        method_name_a,
                        method_name_b,
                        extra_ns_method_names,
                        method_sub_sections,
                    });
                }
                // method-parameter-section
                (2, "p") => {
                    let lv_index = words
                        .next()
                        .ok_or(TinyV2Error::new("Method parameter must have a lv-index"))?
                        .parse()
                        .map_err(|_| TinyV2Error::new("lv-index must be a non negative int"))?;
                    let var_name_a = words.next().filter(|s| !s.is_empty());
                    let var_name_b = words.next().filter(|s| !s.is_empty());
                    let extra_ns_var_names = words.collect();
                    let method_parameter_sub_sections =
                        std::mem::take(&mut sections.method_member_sub_sections);
                    sections
                        .method_sub_sections
                        .push(MethodSubSection::Parameter {
                            lv_index,
                            var_name_a,
                            var_name_b,
                            extra_ns_var_names,
                            method_parameter_sub_sections,
                        });
                }
                // method-variable-section
                (2, "v") => {
                    let lv_index = words
                        .next()
                        .ok_or(TinyV2Error::new("Method variable must have a lv-index"))?
                        .parse()
                        .map_err(|_| TinyV2Error::new("lv-index must be a non negative int"))?;
                    let lv_start_offset = words
                        .next()
                        .ok_or(TinyV2Error::new(
                            "Method variable must have a lv-start-offset",
                        ))?
                        .parse()
                        .map_err(|_| {
                            TinyV2Error::new("lv-start-offset must be a non negative int")
                        })?;
                    let optional_lvt_index = words
                        .next()
                        .ok_or(TinyV2Error::new("Method variable must have a lvt-index"))?
                        .parse()
                        .map_err(|_| {
                            TinyV2Error::new("lvt-index must be signed integer with 32 bits")
                        })?;
                    if optional_lvt_index < -1 {
                        return Err(TinyV2Error::new("lvt-index must be positive integer or -1"));
                    }
                    let var_name_a = words.next().filter(|s| !s.is_empty());
                    let var_name_b = words.next().filter(|s| !s.is_empty());
                    let extra_ns_var_names = words.collect();
                    let method_variable_sub_sections =
                        std::mem::take(&mut sections.method_member_sub_sections);
                    sections
                        .method_sub_sections
                        .push(MethodSubSection::Variable {
                            lv_index,
                            lv_start_offset,
                            optional_lvt_index,
                            var_name_a,
                            var_name_b,
                            extra_ns_var_names,
                            method_variable_sub_sections,
                        });
                }
                // property
                (1, key) => {
                    let value = words.next();
                    sections.properties.push(Property { key, value });
                }
                // unknown / invalid
                (lt, id) => {
                    return Err(TinyV2Error(format!(
                        "Wrong indentation or identifier (Tab size: {}, Identifier: {})",
                        lt, id,
                    )));
                }
            }
        }
        Err(TinyV2Error::new("Header not found!"))
    }
}

/// This method parses the entire string from a tiny file
pub fn from_str(s: &str) -> Result<File<'_>, TinyV2Error> {
    File::from_str(s)
}