oslquery_petite/parser/
mod.rs

1//! OSO file parser module.
2//!
3//! This module provides the tokenization and parsing functionality for OSO files.
4//! The parser uses a line-by-line, token-based approach that matches the behavior
5//! of OpenShadingLanguage's C++ parser.
6
7/// Hint parsing utilities for metadata extraction.
8pub mod hint;
9/// Core OSO tokenization and parsing functions.
10pub mod oso;
11/// Main reader implementation that orchestrates the parsing.
12pub mod reader;
13/// Intermediate types for parsing.
14pub mod types;
15
16pub use reader::OsoReader;
17
18use ariadne::{Color, Label, Report, ReportKind, Source};
19use thiserror::Error;
20
21/// Errors that can occur during OSO file parsing.
22///
23/// These errors provide detailed information about what went wrong during
24/// parsing, including line numbers for parse errors and pretty-printed
25/// error messages when source code is available.
26#[derive(Debug, Clone, Error, PartialEq, Eq)]
27pub enum ParseError {
28    #[error("IO error: {0}")]
29    Io(String),
30
31    #[error("Invalid OSO file format: {0}")]
32    InvalidFormat(String),
33
34    #[error("Unsupported OSO version: {major}.{minor}")]
35    UnsupportedVersion { major: i32, minor: i32 },
36
37    #[error("Parse error at line {line}: {message}")]
38    ParseError {
39        line: usize,
40        message: String,
41        /// Optional token that caused the error and its position in the line
42        token_info: Option<(String, usize)>,
43    },
44
45    #[error("Incomplete parse: {0}")]
46    Incomplete(String),
47
48    #[error("Conversion error: {0}")]
49    Conversion(String),
50}
51
52// Manual Hash implementation for ParseError when hash feature is enabled
53#[cfg(feature = "hash")]
54impl std::hash::Hash for ParseError {
55    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
56        std::mem::discriminant(self).hash(state);
57        match self {
58            ParseError::Io(s) => s.hash(state),
59            ParseError::InvalidFormat(s) => s.hash(state),
60            ParseError::UnsupportedVersion { major, minor } => {
61                major.hash(state);
62                minor.hash(state);
63            }
64            ParseError::ParseError {
65                line,
66                message,
67                token_info,
68            } => {
69                line.hash(state);
70                message.hash(state);
71                token_info.hash(state);
72            }
73            ParseError::Incomplete(s) => s.hash(state),
74            ParseError::Conversion(s) => s.hash(state),
75        }
76    }
77}
78
79// Convert from io::Error
80impl From<std::io::Error> for ParseError {
81    fn from(err: std::io::Error) -> Self {
82        ParseError::Io(err.to_string())
83    }
84}
85
86impl ParseError {
87    /// Print the error with ariadne for nice formatting.
88    pub fn print_with_source(&self, filename: &str, source: &str) -> std::io::Result<()> {
89        match self {
90            ParseError::ParseError {
91                line,
92                message,
93                token_info,
94            } => {
95                // Calculate byte offset from line number
96                let mut line_start_offset = 0;
97                let mut current_line = 1;
98                for (i, ch) in source.char_indices() {
99                    if current_line == *line {
100                        line_start_offset = i;
101                        break;
102                    }
103                    if ch == '\n' {
104                        current_line += 1;
105                    }
106                }
107
108                // Get the line content
109                let line_content = source[line_start_offset..].lines().next().unwrap_or("");
110
111                // Calculate the span for the error
112                let (start_offset, end_offset) = if let Some((token, _token_pos)) = token_info {
113                    // Find the token in the line and highlight just that token
114                    if let Some(token_idx) = line_content.find(token.as_str()) {
115                        let token_start = line_start_offset + token_idx;
116                        let token_end = token_start + token.len();
117                        (token_start, token_end)
118                    } else {
119                        // Fallback to whole line if token not found
120                        let line_end = line_start_offset + line_content.len();
121                        (line_start_offset, line_end)
122                    }
123                } else {
124                    // No token info, highlight whole line
125                    let line_end = line_start_offset + line_content.len();
126                    (line_start_offset, line_end)
127                };
128
129                Report::build(ReportKind::Error, (filename, start_offset..end_offset))
130                    .with_message(format!("Parse error: {}", message))
131                    .with_label(
132                        Label::new((filename, start_offset..end_offset))
133                            .with_message(message)
134                            .with_color(Color::Red),
135                    )
136                    .finish()
137                    .print((filename, Source::from(source)))
138            }
139            ParseError::UnsupportedVersion { major, minor } => {
140                Report::build(ReportKind::Error, (filename, 0..0))
141                    .with_message(format!("Unsupported OSO version: {}.{}", major, minor))
142                    .with_note("This parser supports OSO version 1.11 and above")
143                    .finish()
144                    .print((filename, Source::from(source)))
145            }
146            _ => {
147                // For other errors, print without source location
148                eprintln!("Error: {}", self);
149                Ok(())
150            }
151        }
152    }
153}