use crate::types::{Diagnostic, DiagnosticCode, LdrawLimits, ParseError, Severity, Span}; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum TokenKind { Zero, One, Two, Three, Four, Five, Step, Rotstep, Pause, Write, Clear, Save, Colour, Code, Value, Edge, Alpha, Luminance, Chrome, Pearlescent, Rubber, MatteMetallic, Metal, Material, File, NoFile, Bfc, Certify, NoCertify, Clip, NoClip, InvertNext, Orientation, Texmap, Start, Next, Fallback, End, Stop, Number, Identifier, QuotedIdentifier, Garbage, } #[derive(Clone, Copy, Debug, PartialEq)] pub struct Token<'src> { pub kind: TokenKind, pub text: &'src str, pub number: Option, pub span: Span, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)] pub enum LineEnding { Lf, CrLf, Cr, None, } impl LineEnding { pub(crate) fn as_str(&self) -> &'static str { match self { Self::Lf => "\n", Self::CrLf => "\r\n", Self::Cr => "\r", Self::None => "", } } pub fn as_bytes(&self) -> &'static [u8] { self.as_str().as_bytes() } } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum LineType { Zero, One, Two, Three, Four, Five, } impl From for u8 { fn from(lt: LineType) -> u8 { match lt { LineType::Zero => 0, LineType::One => 1, LineType::Two => 2, LineType::Three => 3, LineType::Four => 4, LineType::Five => 5, } } } impl From for LineType { fn from(v: u8) -> Self { match v { 0 => LineType::Zero, 1 => LineType::One, 2 => LineType::Two, 3 => LineType::Three, 4 => LineType::Four, 5 => LineType::Five, _ => panic!("invalid line type: {v}"), } } } impl serde::Serialize for LineType { fn serialize(&self, s: S) -> Result { s.serialize_u8((*self).into()) } } impl<'de> serde::Deserialize<'de> for LineType { fn deserialize>(d: D) -> Result { let v = u8::deserialize(d)?; match v { 0 => Ok(LineType::Zero), 1 => Ok(LineType::One), 2 => Ok(LineType::Two), 3 => Ok(LineType::Three), 4 => Ok(LineType::Four), 5 => Ok(LineType::Five), _ => Err(serde::de::Error::custom(format!("invalid line type: {v}"))), } } } #[derive(Clone, Debug, PartialEq)] pub struct ScannedLine<'src> { pub line_type: Option, pub raw: &'src [u8], pub raw_tail: &'src [u8], pub tokens: Vec>, pub span: Span, pub ending: LineEnding, pub blank: bool, } fn keyword(text: &str) -> Option { Some(match text { "0" => TokenKind::Zero, "1" => TokenKind::One, "2" => TokenKind::Two, "3" => TokenKind::Three, "4" => TokenKind::Four, "5" => TokenKind::Five, "STEP" => TokenKind::Step, "ROTSTEP" => TokenKind::Rotstep, "PAUSE" => TokenKind::Pause, "WRITE" => TokenKind::Write, "CLEAR" => TokenKind::Clear, "SAVE" => TokenKind::Save, "!COLOUR" => TokenKind::Colour, "CODE" => TokenKind::Code, "VALUE" => TokenKind::Value, "EDGE" => TokenKind::Edge, "ALPHA" => TokenKind::Alpha, "LUMINANCE" => TokenKind::Luminance, "CHROME" => TokenKind::Chrome, "PEARLESCENT" => TokenKind::Pearlescent, "RUBBER" => TokenKind::Rubber, "MATTE_METALLIC" => TokenKind::MatteMetallic, "METAL" => TokenKind::Metal, "MATERIAL" => TokenKind::Material, "FILE" => TokenKind::File, "NOFILE" => TokenKind::NoFile, "BFC" => TokenKind::Bfc, "CERTIFY" => TokenKind::Certify, "NOCERTIFY" => TokenKind::NoCertify, "CLIP" => TokenKind::Clip, "NOCLIP" => TokenKind::NoClip, "INVERTNEXT" => TokenKind::InvertNext, "CW" | "CCW" => TokenKind::Orientation, "!TEXMAP" => TokenKind::Texmap, "START" => TokenKind::Start, "NEXT" => TokenKind::Next, "FALLBACK" => TokenKind::Fallback, "END" => TokenKind::End, "STOP" => TokenKind::Stop, _ => return None, }) } pub fn scan_lines<'src>( bytes: &'src [u8], limits: &LdrawLimits, ) -> Result>, ParseError> { let mut out = Vec::new(); let mut offset = 0usize; let mut line_no = 1u32; while offset < bytes.len() { let start = offset; let mut end = offset; let ending; while end < bytes.len() && bytes[end] != b'\n' && bytes[end] != b'\r' { end += 1; } if end == bytes.len() { ending = LineEnding::None; offset = end; } else if bytes[end] == b'\r' && end + 1 < bytes.len() && bytes[end + 1] == b'\n' { ending = LineEnding::CrLf; offset = end + 2; } else { ending = if bytes[end] == b'\r' { LineEnding::Cr } else { LineEnding::Lf }; offset = end + 1; } let raw = &bytes[start..end]; let line_len = u64::try_from(raw.len()).map_err(|_| ParseError::Overflow("line bytes"))?; let start_u32 = u32::try_from(start).map_err(|_| ParseError::Overflow("line span start"))?; let end_u32 = u32::try_from(end).map_err(|_| ParseError::Overflow("line span end"))?; if line_len > limits.line_bytes { return Err(ParseError::Diagnostic(Diagnostic { code: DiagnosticCode::LineBytesLimit, severity: Severity::Error, message: "physical line exceeds the configured limit".into(), span: Some(Span { start: start_u32, end: end_u32, line: line_no, column: 1, }), })); } let text = std::str::from_utf8(raw).map_err(|_| { ParseError::Diagnostic(Diagnostic { code: DiagnosticCode::InvalidUtf8, severity: Severity::Error, message: "line is not valid UTF-8".into(), span: Some(Span { start: start_u32, end: end_u32, line: line_no, column: 1, }), }) })?; out.push(scan_one(text, raw, start_u32, line_no, ending)?); line_no = line_no .checked_add(1) .ok_or(ParseError::Overflow("line number"))?; } if bytes.is_empty() { out.push(scan_one("", &[], 0, 1, LineEnding::None)?); } Ok(out) } fn scan_one<'src>( text: &'src str, raw: &'src [u8], start: u32, line: u32, ending: LineEnding, ) -> Result, ParseError> { let trimmed = text.trim(); let blank = trimmed.is_empty(); if text .bytes() .any(|byte| byte.is_ascii_control() && !byte.is_ascii_whitespace()) { return Err(ParseError::Diagnostic(Diagnostic { code: DiagnosticCode::InvalidLineType, severity: Severity::Error, message: "line contains an ASCII control character".into(), span: Some(Span { start, end: start .checked_add( u32::try_from(raw.len()) .map_err(|_| ParseError::Overflow("control character span end"))?, ) .ok_or(ParseError::Overflow("control character span end"))?, line, column: 1, }), })); } let mut tokens = Vec::new(); let mut i = 0usize; let bytes = text.as_bytes(); while i < bytes.len() { while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; } if i >= bytes.len() { break; } let begin = i; let value = if bytes[i] == b'"' { i += 1; let inner = i; while i < bytes.len() && bytes[i] != b'"' { i += 1; } if i >= bytes.len() { return Err(ParseError::Diagnostic(Diagnostic { code: DiagnosticCode::InvalidLineType, severity: Severity::Error, message: "unterminated quoted token".into(), span: Some(Span { start, end: start .checked_add( u32::try_from(bytes.len()) .map_err(|_| ParseError::Overflow("quoted token span end"))?, ) .ok_or(ParseError::Overflow("quoted token span end"))?, line, column: u32::try_from(begin) .map_err(|_| ParseError::Overflow("quoted token column"))? .checked_add(1) .ok_or(ParseError::Overflow("quoted token column"))?, }), })); } let value = &text[inner..i]; i += 1; let begin_u32 = u32::try_from(begin).map_err(|_| ParseError::Overflow("token span start"))?; let end_u32 = u32::try_from(i).map_err(|_| ParseError::Overflow("token span end"))?; let column = begin_u32 .checked_add(1) .ok_or(ParseError::Overflow("token column"))?; let span = Span { start: start .checked_add(begin_u32) .ok_or(ParseError::Overflow("token span start"))?, end: start .checked_add(end_u32) .ok_or(ParseError::Overflow("token span end"))?, line, column, }; tokens.push(Token { kind: TokenKind::QuotedIdentifier, text: value, number: None, span, }); continue; } else { while i < bytes.len() && !bytes[i].is_ascii_whitespace() { i += 1; } &text[begin..i] }; let number = value.parse::().ok().filter(|n| n.is_finite()); let kind = keyword(value) .or_else(|| number.map(|_| TokenKind::Number)) .unwrap_or(TokenKind::Identifier); let begin_u32 = u32::try_from(begin).map_err(|_| ParseError::Overflow("token span start"))?; let end_u32 = u32::try_from(i).map_err(|_| ParseError::Overflow("token span end"))?; let column = begin_u32 .checked_add(1) .ok_or(ParseError::Overflow("token column"))?; tokens.push(Token { kind, text: value, number, span: Span { start: start .checked_add(begin_u32) .ok_or(ParseError::Overflow("token span start"))?, end: start .checked_add(end_u32) .ok_or(ParseError::Overflow("token span end"))?, line, column, }, }); } let line_type = tokens.first().and_then(|t| match t.kind { TokenKind::Zero => Some(LineType::Zero), TokenKind::One => Some(LineType::One), TokenKind::Two => Some(LineType::Two), TokenKind::Three => Some(LineType::Three), TokenKind::Four => Some(LineType::Four), TokenKind::Five => Some(LineType::Five), _ => None, }); let tail_start = tokens .first() .and_then(|t| usize::try_from(t.span.end.checked_sub(start)?).ok()) .unwrap_or(0); let raw_tail = raw.get(tail_start..).unwrap_or_default(); Ok(ScannedLine { line_type, raw, raw_tail, tokens, span: Span { start, end: start .checked_add( u32::try_from(raw.len()).map_err(|_| ParseError::Overflow("line span end"))?, ) .ok_or(ParseError::Overflow("line span end"))?, line, column: 1, }, ending, blank, }) }