Skip to main content

provenant/parsers/
meson.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5use std::path::Path;
6
7use crate::parser_warn as warn;
8use packageurl::PackageUrl;
9use serde_json::Value as JsonValue;
10
11use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
12
13use super::PackageParser;
14use super::license_normalization::normalize_spdx_declared_license;
15use super::metadata::ParserMetadata;
16use super::utils::{
17    MAX_ITERATION_COUNT, RecursionGuard, capped_iteration_limit, read_file_to_string,
18    truncate_field,
19};
20
21pub struct MesonParser;
22
23impl PackageParser for MesonParser {
24    const PACKAGE_TYPE: PackageType = PackageType::Meson;
25
26    fn is_match(path: &Path) -> bool {
27        path.file_name().is_some_and(|name| name == "meson.build")
28    }
29
30    fn metadata() -> Vec<ParserMetadata> {
31        vec![ParserMetadata {
32            description: "Meson meson.build manifest",
33            file_patterns: &["**/meson.build"],
34            package_type: "meson",
35            primary_language: "",
36            documentation_url: Some("https://mesonbuild.com/Syntax.html"),
37        }]
38    }
39
40    fn extract_packages(path: &Path) -> Vec<PackageData> {
41        let content = match read_file_to_string(path, None) {
42            Ok(content) => content,
43            Err(error) => {
44                warn!("Failed to read meson.build at {:?}: {}", path, error);
45                return vec![default_package_data()];
46            }
47        };
48
49        match parse_meson_build(&content) {
50            Ok(package) => vec![package],
51            Err(_) => vec![default_package_data()],
52        }
53    }
54}
55
56fn parse_meson_build(content: &str) -> Result<PackageData, String> {
57    let sanitized = strip_comments(content)?;
58    let statements = split_statements(&sanitized);
59
60    let mut package = default_package_data();
61    let mut extra_data = HashMap::new();
62    let mut dependencies = Vec::new();
63    let mut control_flow_depth = 0usize;
64
65    let statement_limit = capped_iteration_limit(statements.len(), "meson: top-level statements");
66    for statement in statements.into_iter().take(statement_limit) {
67        let trimmed = statement.trim();
68        if trimmed.is_empty() {
69            continue;
70        }
71
72        if is_block_closer(trimmed) {
73            control_flow_depth = control_flow_depth.saturating_sub(1);
74            continue;
75        }
76
77        if control_flow_depth > 0 {
78            if is_block_opener(trimmed) {
79                control_flow_depth += 1;
80            }
81            continue;
82        }
83
84        if is_block_opener(trimmed) {
85            control_flow_depth += 1;
86            continue;
87        }
88
89        let Ok(parsed) = parse_statement(trimmed) else {
90            continue;
91        };
92        match parsed {
93            Statement::Expr(expr) | Statement::Assignment(expr) => {
94                handle_top_level_expr(&expr, &mut package, &mut extra_data, &mut dependencies)
95            }
96        }
97    }
98
99    package.dependencies = dependencies;
100    package.extra_data = (!extra_data.is_empty()).then_some(extra_data);
101    package.purl = package
102        .name
103        .as_deref()
104        .and_then(|name| build_project_purl(name, package.version.as_deref()));
105
106    Ok(package)
107}
108
109fn handle_top_level_expr(
110    expr: &Expr,
111    package: &mut PackageData,
112    extra_data: &mut HashMap<String, JsonValue>,
113    dependencies: &mut Vec<Dependency>,
114) {
115    let Expr::Call(call) = expr else {
116        return;
117    };
118
119    match call.name.as_str() {
120        "project" if package.name.is_none() => apply_project_call(call, package, extra_data),
121        "dependency" => dependencies.extend(extract_dependencies_from_call(call)),
122        _ => {}
123    }
124}
125
126fn apply_project_call(
127    call: &CallExpr,
128    package: &mut PackageData,
129    extra_data: &mut HashMap<String, JsonValue>,
130) {
131    let Some(name) = call.positional.first().and_then(expr_as_string) else {
132        return;
133    };
134
135    package.package_type = Some(PackageType::Meson);
136    package.datasource_id = Some(DatasourceId::MesonBuild);
137    package.name = Some(truncate_field(name.to_string()));
138
139    let languages = call
140        .positional
141        .iter()
142        .skip(1)
143        .flat_map(extract_string_values)
144        .collect::<Vec<_>>();
145    if let Some(primary_language) = languages.first() {
146        package.primary_language = Some(truncate_field(primary_language.clone()));
147    }
148    if !languages.is_empty() {
149        extra_data.insert(
150            "languages".to_string(),
151            JsonValue::Array(languages.iter().cloned().map(JsonValue::String).collect()),
152        );
153    }
154
155    if let Some(version) = call.keyword.get("version").and_then(expr_as_string) {
156        package.version = Some(truncate_field(version.to_string()));
157    }
158
159    let licenses = call
160        .keyword
161        .get("license")
162        .map(extract_string_values)
163        .unwrap_or_default();
164    if !licenses.is_empty() {
165        package.extracted_license_statement = Some(truncate_field(licenses.join("\n")));
166        if licenses.len() == 1 {
167            let (declared_license_expression, declared_license_expression_spdx, license_detections) =
168                normalize_spdx_declared_license(licenses.first().map(String::as_str));
169            package.declared_license_expression = declared_license_expression;
170            package.declared_license_expression_spdx = declared_license_expression_spdx;
171            package.license_detections = license_detections;
172        }
173    }
174
175    let license_files = call
176        .keyword
177        .get("license_files")
178        .map(extract_string_values)
179        .unwrap_or_default();
180    if !license_files.is_empty() {
181        extra_data.insert(
182            "license_files".to_string(),
183            JsonValue::Array(license_files.into_iter().map(JsonValue::String).collect()),
184        );
185    }
186
187    if let Some(meson_version) = call.keyword.get("meson_version").and_then(expr_as_string) {
188        extra_data.insert(
189            "meson_version".to_string(),
190            JsonValue::String(meson_version.to_string()),
191        );
192    }
193}
194
195fn extract_dependencies_from_call(call: &CallExpr) -> Vec<Dependency> {
196    let dependency_names = call
197        .positional
198        .iter()
199        .filter_map(expr_as_string)
200        // Skip non-literal or empty names such as `dependency('')`; emitting a
201        // nameless `pkg:generic/meson/` identity is junk, not honest evidence.
202        .filter(|name| !name.trim().is_empty())
203        .map(ToOwned::to_owned)
204        .collect::<Vec<_>>();
205
206    if dependency_names.is_empty() {
207        return Vec::new();
208    }
209
210    let extracted_requirement = call.keyword.get("version").map(|expr| {
211        extract_string_values(expr)
212            .into_iter()
213            .filter(|value| !value.is_empty())
214            .collect::<Vec<_>>()
215            .join(", ")
216    });
217    let required = call.keyword.get("required").and_then(expr_as_bool);
218    let native = call.keyword.get("native").and_then(expr_as_bool);
219
220    let dependency_limit =
221        capped_iteration_limit(dependency_names.len(), "meson: dependency() names");
222    dependency_names
223        .into_iter()
224        .take(dependency_limit)
225        .map(|name| {
226            let mut extra_data = HashMap::new();
227
228            if let Some(requirement) = extracted_requirement
229                .as_ref()
230                .filter(|value| !value.is_empty())
231            {
232                extra_data.insert(
233                    "version".to_string(),
234                    JsonValue::String(requirement.clone()),
235                );
236            }
237            if let Some(required) = required {
238                extra_data.insert("required".to_string(), JsonValue::Bool(required));
239            }
240            if let Some(method) = call.keyword.get("method").and_then(expr_as_string) {
241                extra_data.insert("method".to_string(), JsonValue::String(method.to_string()));
242            }
243            if let Some(native) = native {
244                extra_data.insert("native".to_string(), JsonValue::Bool(native));
245            }
246
247            let modules = call
248                .keyword
249                .get("modules")
250                .map(extract_string_values)
251                .unwrap_or_default();
252            if !modules.is_empty() {
253                extra_data.insert(
254                    "modules".to_string(),
255                    JsonValue::Array(modules.into_iter().map(JsonValue::String).collect()),
256                );
257            }
258
259            let fallback = call
260                .keyword
261                .get("fallback")
262                .map(extract_string_values)
263                .unwrap_or_default();
264            if !fallback.is_empty() {
265                extra_data.insert(
266                    "fallback".to_string(),
267                    JsonValue::Array(fallback.into_iter().map(JsonValue::String).collect()),
268                );
269            }
270
271            Dependency {
272                purl: build_dependency_purl(&name),
273                extracted_requirement: extracted_requirement
274                    .clone()
275                    .filter(|value| !value.is_empty())
276                    .map(truncate_field),
277                scope: Some("dependencies".to_string()),
278                is_runtime: Some(native != Some(true)),
279                is_optional: Some(required == Some(false)),
280                is_pinned: Some(false),
281                is_direct: Some(true),
282                resolved_package: None,
283                extra_data: (!extra_data.is_empty()).then_some(extra_data),
284            }
285        })
286        .collect()
287}
288
289fn build_project_purl(name: &str, version: Option<&str>) -> Option<String> {
290    let mut purl = PackageUrl::new(PackageType::Meson.as_str(), name).ok()?;
291    if let Some(version) = version {
292        purl.with_version(version).ok()?;
293    }
294    Some(truncate_field(purl.to_string()))
295}
296
297fn build_dependency_purl(name: &str) -> Option<String> {
298    let mut purl = PackageUrl::new("generic", name).ok()?;
299    purl.with_namespace("meson").ok()?;
300    Some(truncate_field(purl.to_string()))
301}
302
303fn default_package_data() -> PackageData {
304    PackageData {
305        package_type: Some(PackageType::Meson),
306        datasource_id: Some(DatasourceId::MesonBuild),
307        ..Default::default()
308    }
309}
310
311fn is_block_opener(statement: &str) -> bool {
312    matches!(
313        statement.split_whitespace().next(),
314        Some("if") | Some("foreach")
315    )
316}
317
318fn is_block_closer(statement: &str) -> bool {
319    matches!(statement.trim(), "endif" | "endforeach")
320}
321
322fn strip_comments(input: &str) -> Result<String, String> {
323    let chars: Vec<char> = input.chars().collect();
324    let mut output = String::with_capacity(input.len());
325    let mut index = 0usize;
326    let mut in_string = false;
327    let mut string_delimiter = '\0';
328    let mut escaped = false;
329    let mut chars_processed = 0usize;
330
331    while index < chars.len() {
332        chars_processed += 1;
333        if chars_processed > MAX_ITERATION_COUNT {
334            break;
335        }
336        let ch = chars[index];
337
338        if in_string {
339            output.push(ch);
340            if escaped {
341                escaped = false;
342            } else if ch == '\\' {
343                escaped = true;
344            } else if ch == string_delimiter {
345                in_string = false;
346            }
347            index += 1;
348            continue;
349        }
350
351        if matches!(ch, '\'' | '"') {
352            in_string = true;
353            string_delimiter = ch;
354            output.push(ch);
355            index += 1;
356            continue;
357        }
358
359        if ch == '#' {
360            index += 1;
361            while index < chars.len() && chars[index] != '\n' {
362                index += 1;
363            }
364            continue;
365        }
366
367        output.push(ch);
368        index += 1;
369    }
370
371    if in_string {
372        return Err("unterminated string literal".to_string());
373    }
374
375    Ok(output)
376}
377
378fn split_statements(input: &str) -> Vec<String> {
379    let mut statements = Vec::new();
380    let mut current = String::new();
381    let mut paren_depth = 0usize;
382    let mut bracket_depth = 0usize;
383    let mut in_string = false;
384    let mut string_delimiter = '\0';
385    let mut escaped = false;
386    let mut chars_processed = 0usize;
387
388    for ch in input.chars() {
389        chars_processed += 1;
390        if chars_processed > MAX_ITERATION_COUNT {
391            break;
392        }
393        current.push(ch);
394
395        if in_string {
396            if escaped {
397                escaped = false;
398            } else if ch == '\\' {
399                escaped = true;
400            } else if ch == string_delimiter {
401                in_string = false;
402            }
403            continue;
404        }
405
406        match ch {
407            '\'' | '"' => {
408                in_string = true;
409                string_delimiter = ch;
410            }
411            '(' => paren_depth += 1,
412            ')' => paren_depth = paren_depth.saturating_sub(1),
413            '[' => bracket_depth += 1,
414            ']' => bracket_depth = bracket_depth.saturating_sub(1),
415            '\n' if paren_depth == 0 && bracket_depth == 0 => {
416                let trimmed = current.trim();
417                if !trimmed.is_empty() {
418                    statements.push(trimmed.to_string());
419                }
420                current.clear();
421            }
422            _ => {}
423        }
424    }
425
426    let trimmed = current.trim();
427    if !trimmed.is_empty() {
428        statements.push(trimmed.to_string());
429    }
430
431    statements
432}
433
434#[derive(Debug, Clone)]
435enum Statement {
436    Expr(Expr),
437    Assignment(Expr),
438}
439
440#[derive(Debug, Clone)]
441enum Expr {
442    String(String),
443    Bool(bool),
444    Array(Vec<Expr>),
445    Identifier,
446    Call(CallExpr),
447}
448
449#[derive(Debug, Clone)]
450struct CallExpr {
451    name: String,
452    positional: Vec<Expr>,
453    keyword: HashMap<String, Expr>,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq)]
457enum Token {
458    Ident(String),
459    Str(String),
460    Bool(bool),
461    LParen,
462    RParen,
463    LBracket,
464    RBracket,
465    Colon,
466    Comma,
467    Equal,
468}
469
470fn parse_statement(statement: &str) -> Result<Statement, String> {
471    let tokens = tokenize(statement)?;
472    if tokens.is_empty() {
473        return Err("empty statement".to_string());
474    }
475
476    if let [Token::Ident(name), Token::Equal, rest @ ..] = tokens.as_slice() {
477        let mut parser = Parser::new(rest);
478        let expr = parser.parse_expr()?;
479        parser.expect_end()?;
480        let _ = name;
481        return Ok(Statement::Assignment(expr));
482    }
483
484    let mut parser = Parser::new(&tokens);
485    let expr = parser.parse_expr()?;
486    parser.expect_end()?;
487    Ok(Statement::Expr(expr))
488}
489
490fn tokenize(input: &str) -> Result<Vec<Token>, String> {
491    let chars: Vec<char> = input.chars().collect();
492    let mut tokens = Vec::new();
493    let mut index = 0usize;
494
495    while index < chars.len() {
496        if tokens.len() >= MAX_ITERATION_COUNT {
497            break;
498        }
499        let ch = chars[index];
500        if ch.is_whitespace() {
501            index += 1;
502            continue;
503        }
504
505        match ch {
506            '(' => {
507                tokens.push(Token::LParen);
508                index += 1;
509            }
510            ')' => {
511                tokens.push(Token::RParen);
512                index += 1;
513            }
514            '[' => {
515                tokens.push(Token::LBracket);
516                index += 1;
517            }
518            ']' => {
519                tokens.push(Token::RBracket);
520                index += 1;
521            }
522            ':' => {
523                tokens.push(Token::Colon);
524                index += 1;
525            }
526            ',' => {
527                tokens.push(Token::Comma);
528                index += 1;
529            }
530            '=' => {
531                tokens.push(Token::Equal);
532                index += 1;
533            }
534            '\'' | '"' => {
535                let delimiter = ch;
536                index += 1;
537                let start = index;
538                let mut escaped = false;
539                while index < chars.len() {
540                    let current = chars[index];
541                    if escaped {
542                        escaped = false;
543                    } else if current == '\\' {
544                        escaped = true;
545                    } else if current == delimiter {
546                        break;
547                    }
548                    index += 1;
549                }
550
551                if index >= chars.len() {
552                    return Err("unterminated string token".to_string());
553                }
554
555                let value: String = chars[start..index].iter().collect();
556                tokens.push(Token::Str(value));
557                index += 1;
558            }
559            _ if is_ident_start(ch) => {
560                let start = index;
561                index += 1;
562                while index < chars.len() && is_ident_continue(chars[index]) {
563                    index += 1;
564                }
565                let ident: String = chars[start..index].iter().collect();
566                match ident.as_str() {
567                    "true" => tokens.push(Token::Bool(true)),
568                    "false" => tokens.push(Token::Bool(false)),
569                    _ => tokens.push(Token::Ident(ident)),
570                }
571            }
572            _ => {
573                return Err(format!("unsupported token '{}'", ch));
574            }
575        }
576    }
577
578    Ok(tokens)
579}
580
581fn is_ident_start(ch: char) -> bool {
582    ch.is_ascii_alphabetic() || ch == '_'
583}
584
585fn is_ident_continue(ch: char) -> bool {
586    ch.is_ascii_alphanumeric() || ch == '_'
587}
588
589struct Parser<'a> {
590    tokens: &'a [Token],
591    index: usize,
592    guard: RecursionGuard<()>,
593}
594
595impl<'a> Parser<'a> {
596    fn new(tokens: &'a [Token]) -> Self {
597        Self {
598            tokens,
599            index: 0,
600            guard: RecursionGuard::depth_only(),
601        }
602    }
603
604    fn parse_expr(&mut self) -> Result<Expr, String> {
605        if self.guard.descend() {
606            return Err("recursion depth exceeded".to_string());
607        }
608        let result = match self.peek() {
609            Some(Token::Str(value)) => {
610                self.index += 1;
611                Ok(Expr::String(value.clone()))
612            }
613            Some(Token::Bool(value)) => {
614                self.index += 1;
615                Ok(Expr::Bool(*value))
616            }
617            Some(Token::LBracket) => self.parse_array(),
618            Some(Token::Ident(_)) => self.parse_identifier_or_call(),
619            Some(token) => Err(format!("unexpected token {:?}", token)),
620            None => Err("unexpected end of input".to_string()),
621        };
622        self.guard.ascend();
623        result
624    }
625
626    fn parse_array(&mut self) -> Result<Expr, String> {
627        self.expect(Token::LBracket)?;
628        let mut values = Vec::new();
629        let mut element_count = 0usize;
630        while !matches!(self.peek(), Some(Token::RBracket)) {
631            element_count += 1;
632            if element_count > MAX_ITERATION_COUNT {
633                break;
634            }
635            let expr = self.parse_expr()?;
636            values.push(expr);
637            if matches!(self.peek(), Some(Token::Comma)) {
638                self.index += 1;
639            } else if !matches!(self.peek(), Some(Token::RBracket)) {
640                return Err("expected ',' or ']' in array".to_string());
641            }
642        }
643        self.expect(Token::RBracket)?;
644        Ok(Expr::Array(values))
645    }
646
647    fn parse_identifier_or_call(&mut self) -> Result<Expr, String> {
648        let Token::Ident(name) = self
649            .next()
650            .cloned()
651            .ok_or_else(|| "expected identifier".to_string())?
652        else {
653            return Err("expected identifier".to_string());
654        };
655
656        if !matches!(self.peek(), Some(Token::LParen)) {
657            let _ = name;
658            return Ok(Expr::Identifier);
659        }
660
661        self.expect(Token::LParen)?;
662        let mut positional = Vec::new();
663        let mut keyword = HashMap::new();
664        let mut arg_count = 0usize;
665
666        while !matches!(self.peek(), Some(Token::RParen)) {
667            arg_count += 1;
668            if arg_count > MAX_ITERATION_COUNT {
669                break;
670            }
671            if let (Some(Token::Ident(arg_name)), Some(Token::Colon)) =
672                (self.peek(), self.peek_n(1))
673            {
674                let arg_name = arg_name.clone();
675                self.index += 2;
676                let value = self.parse_expr()?;
677                keyword.insert(arg_name, value);
678            } else {
679                let expr = self.parse_expr()?;
680                positional.push(expr);
681            }
682
683            if matches!(self.peek(), Some(Token::Comma)) {
684                self.index += 1;
685            } else if !matches!(self.peek(), Some(Token::RParen)) {
686                return Err("expected ',' or ')' in call".to_string());
687            }
688        }
689
690        self.expect(Token::RParen)?;
691        Ok(Expr::Call(CallExpr {
692            name,
693            positional,
694            keyword,
695        }))
696    }
697
698    fn expect(&mut self, expected: Token) -> Result<(), String> {
699        match self.next() {
700            Some(token) if *token == expected => Ok(()),
701            Some(token) => Err(format!("expected {:?}, found {:?}", expected, token)),
702            None => Err(format!("expected {:?}, found end of input", expected)),
703        }
704    }
705
706    fn expect_end(&self) -> Result<(), String> {
707        if self.index == self.tokens.len() {
708            Ok(())
709        } else {
710            Err(format!(
711                "unexpected trailing tokens: {:?}",
712                &self.tokens[self.index..]
713            ))
714        }
715    }
716
717    fn peek(&self) -> Option<&'a Token> {
718        self.tokens.get(self.index)
719    }
720
721    fn peek_n(&self, offset: usize) -> Option<&'a Token> {
722        self.tokens.get(self.index + offset)
723    }
724
725    fn next(&mut self) -> Option<&'a Token> {
726        let token = self.tokens.get(self.index);
727        if token.is_some() {
728            self.index += 1;
729        }
730        token
731    }
732}
733
734fn expr_as_string(expr: &Expr) -> Option<&str> {
735    match expr {
736        Expr::String(value) => Some(value.as_str()),
737        _ => None,
738    }
739}
740
741fn expr_as_bool(expr: &Expr) -> Option<bool> {
742    match expr {
743        Expr::Bool(value) => Some(*value),
744        _ => None,
745    }
746}
747
748fn extract_string_values(expr: &Expr) -> Vec<String> {
749    match expr {
750        Expr::String(value) => vec![value.clone()],
751        Expr::Array(values) => values
752            .iter()
753            .filter_map(expr_as_string)
754            .map(ToOwned::to_owned)
755            .collect(),
756        _ => Vec::new(),
757    }
758}