+trait TextRecord
+where
+ Self: Sized,
+{
+ const NAME: &'static str;
+ fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error>;
+}
+
+pub struct VariableSet {
+ pub name: String,
+ pub vars: Vec<String>,
+}
+
+impl VariableSet {
+ fn parse(input: &str) -> Result<Self, Error> {
+ let (name, input) = input.split_once('=').ok_or(Error::TBD)?;
+ let vars = input.split_ascii_whitespace().map(String::from).collect();
+ Ok(VariableSet {
+ name: name.into(),
+ vars,
+ })
+ }
+}
+
+pub struct VariableSetRecord(Vec<VariableSet>);
+
+impl TextRecord for VariableSetRecord {
+ const NAME: &'static str = "variable set";
+ fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
+ let mut sets = Vec::new();
+ for line in input.lines() {
+ match VariableSet::parse(line) {
+ Ok(set) => sets.push(set),
+ Err(error) => warn(error),
+ }
+ }
+ Ok(VariableSetRecord(sets))
+ }
+}
+
+pub struct ProductInfo(pub String);
+
+impl TextRecord for ProductInfo {
+ const NAME: &'static str = "extra product info";
+ fn parse(input: &str, _warn: impl Fn(Error)) -> Result<Self, Error> {
+ Ok(ProductInfo(input.into()))
+ }
+}
+
+pub struct LongVariableName {
+ pub short_name: String,
+ pub long_name: String,
+}
+
+pub struct LongVariableNameRecord(Vec<LongVariableName>);
+
+impl TextRecord for LongVariableNameRecord {
+ const NAME: &'static str = "long variable names";
+ fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
+ let mut names = Vec::new();
+ for pair in input.split('\t').filter(|s| !s.is_empty()) {
+ if let Some((short_name, long_name)) = pair.split_once('=') {
+ let name = LongVariableName {
+ short_name: short_name.into(),
+ long_name: long_name.into(),
+ };
+ names.push(name);
+ } else {
+ warn(Error::TBD)
+ }
+ }
+ Ok(LongVariableNameRecord(names))
+ }
+}
+
+pub struct VeryLongString {
+ pub short_name: String,
+ pub length: usize,
+}
+
+impl VeryLongString {
+ fn parse(input: &str) -> Result<VeryLongString, Error> {
+ let Some((short_name, length)) = input.split_once('=') else {
+ return Err(Error::TBD);
+ };
+ let length: usize = length.parse().map_err(|_| Error::TBD)?;
+ Ok(VeryLongString {
+ short_name: short_name.into(),
+ length,
+ })
+ }
+}
+
+pub struct VeryLongStringRecord(Vec<VeryLongString>);
+
+impl TextRecord for VeryLongStringRecord {
+ const NAME: &'static str = "very long strings";
+ fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
+ let mut very_long_strings = Vec::new();
+ for tuple in input
+ .split('\0')
+ .map(|s| s.trim_end_matches('\t'))
+ .filter(|s| !s.is_empty())
+ {
+ match VeryLongString::parse(tuple) {
+ Ok(vls) => very_long_strings.push(vls),
+ Err(error) => warn(error),
+ }
+ }
+ Ok(VeryLongStringRecord(very_long_strings))
+ }
+}
+
+pub struct Attribute {
+ pub name: String,
+ pub values: Vec<String>,
+}
+
+impl Attribute {
+ fn parse<'a>(input: &'a str, warn: &impl Fn(Error)) -> Result<(Attribute, &'a str), Error> {
+ let Some((name, mut input)) = input.split_once('(') else {
+ return Err(Error::TBD);
+ };
+ let mut values = Vec::new();
+ loop {
+ let Some((value, rest)) = input.split_once('\n') else {
+ return Err(Error::TBD);
+ };
+ if let Some(stripped) = value
+ .strip_prefix('\'')
+ .and_then(|value| value.strip_suffix('\''))
+ {
+ values.push(stripped.into());
+ } else {
+ warn(Error::TBD);
+ values.push(value.into());
+ }
+ if let Some(rest) = rest.strip_prefix(')') {
+ return Ok((
+ Attribute {
+ name: name.into(),
+ values,
+ },
+ rest,
+ ));
+ }
+ input = rest;
+ }
+ }
+}
+
+pub struct AttributeSet(pub Vec<Attribute>);
+
+impl AttributeSet {
+ fn parse<'a>(
+ mut input: &'a str,
+ sentinel: Option<char>,
+ warn: &impl Fn(Error),
+ ) -> Result<(AttributeSet, &'a str), Error> {
+ let mut attributes = Vec::new();
+ let rest = loop {
+ match input.chars().next() {
+ None => break input,
+ c if c == sentinel => break &input[1..],
+ _ => {
+ let (attribute, rest) = Attribute::parse(input, &warn)?;
+ attributes.push(attribute);
+ input = rest;
+ }
+ }
+ };
+ Ok((AttributeSet(attributes), rest))
+ }
+}
+
+pub struct FileAttributeRecord(AttributeSet);
+
+impl TextRecord for FileAttributeRecord {
+ const NAME: &'static str = "data file attributes";
+ fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
+ let (set, rest) = AttributeSet::parse(input, None, &warn)?;
+ if !rest.is_empty() {
+ warn(Error::TBD);
+ }
+ Ok(FileAttributeRecord(set))
+ }
+}
+
+pub struct VarAttributeSet {
+ pub long_var_name: String,
+ pub attributes: AttributeSet,
+}
+
+impl VarAttributeSet {
+ fn parse<'a>(
+ input: &'a str,
+ warn: &impl Fn(Error),
+ ) -> Result<(VarAttributeSet, &'a str), Error> {
+ let Some((long_var_name, rest)) = input.split_once(':') else {
+ return Err(Error::TBD);
+ };
+ let (attributes, rest) = AttributeSet::parse(rest, Some('/'), warn)?;
+ Ok((
+ VarAttributeSet {
+ long_var_name: long_var_name.into(),
+ attributes,
+ },
+ rest,
+ ))
+ }
+}
+
+pub struct VariableAttributeRecord(Vec<VarAttributeSet>);
+
+impl TextRecord for VariableAttributeRecord {
+ const NAME: &'static str = "variable attributes";
+ fn parse(mut input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
+ let mut var_attribute_sets = Vec::new();
+ while !input.is_empty() {
+ match VarAttributeSet::parse(input, &warn) {
+ Ok((var_attribute, rest)) => {
+ var_attribute_sets.push(var_attribute);
+ input = rest;
+ }
+ Err(error) => {
+ warn(error);
+ break;
+ }
+ }
+ }
+ Ok(VariableAttributeRecord(var_attribute_sets))
+ }
+}
+