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        .map(ToOwned::to_owned)
201        .collect::<Vec<_>>();
202
203    if dependency_names.is_empty() {
204        return Vec::new();
205    }
206
207    let extracted_requirement = call.keyword.get("version").map(|expr| {
208        extract_string_values(expr)
209            .into_iter()
210            .filter(|value| !value.is_empty())
211            .collect::<Vec<_>>()
212            .join(", ")
213    });
214    let required = call.keyword.get("required").and_then(expr_as_bool);
215    let native = call.keyword.get("native").and_then(expr_as_bool);
216
217    let dependency_limit =
218        capped_iteration_limit(dependency_names.len(), "meson: dependency() names");
219    dependency_names
220        .into_iter()
221        .take(dependency_limit)
222        .map(|name| {
223            let mut extra_data = HashMap::new();
224
225            if let Some(requirement) = extracted_requirement
226                .as_ref()
227                .filter(|value| !value.is_empty())
228            {
229                extra_data.insert(
230                    "version".to_string(),
231                    JsonValue::String(requirement.clone()),
232                );
233            }
234            if let Some(required) = required {
235                extra_data.insert("required".to_string(), JsonValue::Bool(required));
236            }
237            if let Some(method) = call.keyword.get("method").and_then(expr_as_string) {
238                extra_data.insert("method".to_string(), JsonValue::String(method.to_string()));
239            }
240            if let Some(native) = native {
241                extra_data.insert("native".to_string(), JsonValue::Bool(native));
242            }
243
244            let modules = call
245                .keyword
246                .get("modules")
247                .map(extract_string_values)
248                .unwrap_or_default();
249            if !modules.is_empty() {
250                extra_data.insert(
251                    "modules".to_string(),
252                    JsonValue::Array(modules.into_iter().map(JsonValue::String).collect()),
253                );
254            }
255
256            let fallback = call
257                .keyword
258                .get("fallback")
259                .map(extract_string_values)
260                .unwrap_or_default();
261            if !fallback.is_empty() {
262                extra_data.insert(
263                    "fallback".to_string(),
264                    JsonValue::Array(fallback.into_iter().map(JsonValue::String).collect()),
265                );
266            }
267
268            Dependency {
269                purl: build_dependency_purl(&name),
270                extracted_requirement: extracted_requirement
271                    .clone()
272                    .filter(|value| !value.is_empty())
273                    .map(truncate_field),
274                scope: Some("dependencies".to_string()),
275                is_runtime: Some(native != Some(true)),
276                is_optional: Some(required == Some(false)),
277                is_pinned: Some(false),
278                is_direct: Some(true),
279                resolved_package: None,
280                extra_data: (!extra_data.is_empty()).then_some(extra_data),
281            }
282        })
283        .collect()
284}
285
286fn build_project_purl(name: &str, version: Option<&str>) -> Option<String> {
287    let mut purl = PackageUrl::new(PackageType::Meson.as_str(), name).ok()?;
288    if let Some(version) = version {
289        purl.with_version(version).ok()?;
290    }
291    Some(truncate_field(purl.to_string()))
292}
293
294fn build_dependency_purl(name: &str) -> Option<String> {
295    let mut purl = PackageUrl::new("generic", name).ok()?;
296    purl.with_namespace("meson").ok()?;
297    Some(truncate_field(purl.to_string()))
298}
299
300fn default_package_data() -> PackageData {
301    PackageData {
302        package_type: Some(PackageType::Meson),
303        datasource_id: Some(DatasourceId::MesonBuild),
304        ..Default::default()
305    }
306}
307
308fn is_block_opener(statement: &str) -> bool {
309    matches!(
310        statement.split_whitespace().next(),
311        Some("if") | Some("foreach")
312    )
313}
314
315fn is_block_closer(statement: &str) -> bool {
316    matches!(statement.trim(), "endif" | "endforeach")
317}
318
319fn strip_comments(input: &str) -> Result<String, String> {
320    let chars: Vec<char> = input.chars().collect();
321    let mut output = String::with_capacity(input.len());
322    let mut index = 0usize;
323    let mut in_string = false;
324    let mut string_delimiter = '\0';
325    let mut escaped = false;
326    let mut chars_processed = 0usize;
327
328    while index < chars.len() {
329        chars_processed += 1;
330        if chars_processed > MAX_ITERATION_COUNT {
331            break;
332        }
333        let ch = chars[index];
334
335        if in_string {
336            output.push(ch);
337            if escaped {
338                escaped = false;
339            } else if ch == '\\' {
340                escaped = true;
341            } else if ch == string_delimiter {
342                in_string = false;
343            }
344            index += 1;
345            continue;
346        }
347
348        if matches!(ch, '\'' | '"') {
349            in_string = true;
350            string_delimiter = ch;
351            output.push(ch);
352            index += 1;
353            continue;
354        }
355
356        if ch == '#' {
357            index += 1;
358            while index < chars.len() && chars[index] != '\n' {
359                index += 1;
360            }
361            continue;
362        }
363
364        output.push(ch);
365        index += 1;
366    }
367
368    if in_string {
369        return Err("unterminated string literal".to_string());
370    }
371
372    Ok(output)
373}
374
375fn split_statements(input: &str) -> Vec<String> {
376    let mut statements = Vec::new();
377    let mut current = String::new();
378    let mut paren_depth = 0usize;
379    let mut bracket_depth = 0usize;
380    let mut in_string = false;
381    let mut string_delimiter = '\0';
382    let mut escaped = false;
383    let mut chars_processed = 0usize;
384
385    for ch in input.chars() {
386        chars_processed += 1;
387        if chars_processed > MAX_ITERATION_COUNT {
388            break;
389        }
390        current.push(ch);
391
392        if in_string {
393            if escaped {
394                escaped = false;
395            } else if ch == '\\' {
396                escaped = true;
397            } else if ch == string_delimiter {
398                in_string = false;
399            }
400            continue;
401        }
402
403        match ch {
404            '\'' | '"' => {
405                in_string = true;
406                string_delimiter = ch;
407            }
408            '(' => paren_depth += 1,
409            ')' => paren_depth = paren_depth.saturating_sub(1),
410            '[' => bracket_depth += 1,
411            ']' => bracket_depth = bracket_depth.saturating_sub(1),
412            '\n' if paren_depth == 0 && bracket_depth == 0 => {
413                let trimmed = current.trim();
414                if !trimmed.is_empty() {
415                    statements.push(trimmed.to_string());
416                }
417                current.clear();
418            }
419            _ => {}
420        }
421    }
422
423    let trimmed = current.trim();
424    if !trimmed.is_empty() {
425        statements.push(trimmed.to_string());
426    }
427
428    statements
429}
430
431#[derive(Debug, Clone)]
432enum Statement {
433    Expr(Expr),
434    Assignment(Expr),
435}
436
437#[derive(Debug, Clone)]
438enum Expr {
439    String(String),
440    Bool(bool),
441    Array(Vec<Expr>),
442    Identifier,
443    Call(CallExpr),
444}
445
446#[derive(Debug, Clone)]
447struct CallExpr {
448    name: String,
449    positional: Vec<Expr>,
450    keyword: HashMap<String, Expr>,
451}
452
453#[derive(Debug, Clone, PartialEq, Eq)]
454enum Token {
455    Ident(String),
456    Str(String),
457    Bool(bool),
458    LParen,
459    RParen,
460    LBracket,
461    RBracket,
462    Colon,
463    Comma,
464    Equal,
465}
466
467fn parse_statement(statement: &str) -> Result<Statement, String> {
468    let tokens = tokenize(statement)?;
469    if tokens.is_empty() {
470        return Err("empty statement".to_string());
471    }
472
473    if let [Token::Ident(name), Token::Equal, rest @ ..] = tokens.as_slice() {
474        let mut parser = Parser::new(rest);
475        let expr = parser.parse_expr()?;
476        parser.expect_end()?;
477        let _ = name;
478        return Ok(Statement::Assignment(expr));
479    }
480
481    let mut parser = Parser::new(&tokens);
482    let expr = parser.parse_expr()?;
483    parser.expect_end()?;
484    Ok(Statement::Expr(expr))
485}
486
487fn tokenize(input: &str) -> Result<Vec<Token>, String> {
488    let chars: Vec<char> = input.chars().collect();
489    let mut tokens = Vec::new();
490    let mut index = 0usize;
491
492    while index < chars.len() {
493        if tokens.len() >= MAX_ITERATION_COUNT {
494            break;
495        }
496        let ch = chars[index];
497        if ch.is_whitespace() {
498            index += 1;
499            continue;
500        }
501
502        match ch {
503            '(' => {
504                tokens.push(Token::LParen);
505                index += 1;
506            }
507            ')' => {
508                tokens.push(Token::RParen);
509                index += 1;
510            }
511            '[' => {
512                tokens.push(Token::LBracket);
513                index += 1;
514            }
515            ']' => {
516                tokens.push(Token::RBracket);
517                index += 1;
518            }
519            ':' => {
520                tokens.push(Token::Colon);
521                index += 1;
522            }
523            ',' => {
524                tokens.push(Token::Comma);
525                index += 1;
526            }
527            '=' => {
528                tokens.push(Token::Equal);
529                index += 1;
530            }
531            '\'' | '"' => {
532                let delimiter = ch;
533                index += 1;
534                let start = index;
535                let mut escaped = false;
536                while index < chars.len() {
537                    let current = chars[index];
538                    if escaped {
539                        escaped = false;
540                    } else if current == '\\' {
541                        escaped = true;
542                    } else if current == delimiter {
543                        break;
544                    }
545                    index += 1;
546                }
547
548                if index >= chars.len() {
549                    return Err("unterminated string token".to_string());
550                }
551
552                let value: String = chars[start..index].iter().collect();
553                tokens.push(Token::Str(value));
554                index += 1;
555            }
556            _ if is_ident_start(ch) => {
557                let start = index;
558                index += 1;
559                while index < chars.len() && is_ident_continue(chars[index]) {
560                    index += 1;
561                }
562                let ident: String = chars[start..index].iter().collect();
563                match ident.as_str() {
564                    "true" => tokens.push(Token::Bool(true)),
565                    "false" => tokens.push(Token::Bool(false)),
566                    _ => tokens.push(Token::Ident(ident)),
567                }
568            }
569            _ => {
570                return Err(format!("unsupported token '{}'", ch));
571            }
572        }
573    }
574
575    Ok(tokens)
576}
577
578fn is_ident_start(ch: char) -> bool {
579    ch.is_ascii_alphabetic() || ch == '_'
580}
581
582fn is_ident_continue(ch: char) -> bool {
583    ch.is_ascii_alphanumeric() || ch == '_'
584}
585
586struct Parser<'a> {
587    tokens: &'a [Token],
588    index: usize,
589    guard: RecursionGuard<()>,
590}
591
592impl<'a> Parser<'a> {
593    fn new(tokens: &'a [Token]) -> Self {
594        Self {
595            tokens,
596            index: 0,
597            guard: RecursionGuard::depth_only(),
598        }
599    }
600
601    fn parse_expr(&mut self) -> Result<Expr, String> {
602        if self.guard.descend() {
603            return Err("recursion depth exceeded".to_string());
604        }
605        let result = match self.peek() {
606            Some(Token::Str(value)) => {
607                self.index += 1;
608                Ok(Expr::String(value.clone()))
609            }
610            Some(Token::Bool(value)) => {
611                self.index += 1;
612                Ok(Expr::Bool(*value))
613            }
614            Some(Token::LBracket) => self.parse_array(),
615            Some(Token::Ident(_)) => self.parse_identifier_or_call(),
616            Some(token) => Err(format!("unexpected token {:?}", token)),
617            None => Err("unexpected end of input".to_string()),
618        };
619        self.guard.ascend();
620        result
621    }
622
623    fn parse_array(&mut self) -> Result<Expr, String> {
624        self.expect(Token::LBracket)?;
625        let mut values = Vec::new();
626        let mut element_count = 0usize;
627        while !matches!(self.peek(), Some(Token::RBracket)) {
628            element_count += 1;
629            if element_count > MAX_ITERATION_COUNT {
630                break;
631            }
632            let expr = self.parse_expr()?;
633            values.push(expr);
634            if matches!(self.peek(), Some(Token::Comma)) {
635                self.index += 1;
636            } else if !matches!(self.peek(), Some(Token::RBracket)) {
637                return Err("expected ',' or ']' in array".to_string());
638            }
639        }
640        self.expect(Token::RBracket)?;
641        Ok(Expr::Array(values))
642    }
643
644    fn parse_identifier_or_call(&mut self) -> Result<Expr, String> {
645        let Token::Ident(name) = self
646            .next()
647            .cloned()
648            .ok_or_else(|| "expected identifier".to_string())?
649        else {
650            return Err("expected identifier".to_string());
651        };
652
653        if !matches!(self.peek(), Some(Token::LParen)) {
654            let _ = name;
655            return Ok(Expr::Identifier);
656        }
657
658        self.expect(Token::LParen)?;
659        let mut positional = Vec::new();
660        let mut keyword = HashMap::new();
661        let mut arg_count = 0usize;
662
663        while !matches!(self.peek(), Some(Token::RParen)) {
664            arg_count += 1;
665            if arg_count > MAX_ITERATION_COUNT {
666                break;
667            }
668            if let (Some(Token::Ident(arg_name)), Some(Token::Colon)) =
669                (self.peek(), self.peek_n(1))
670            {
671                let arg_name = arg_name.clone();
672                self.index += 2;
673                let value = self.parse_expr()?;
674                keyword.insert(arg_name, value);
675            } else {
676                let expr = self.parse_expr()?;
677                positional.push(expr);
678            }
679
680            if matches!(self.peek(), Some(Token::Comma)) {
681                self.index += 1;
682            } else if !matches!(self.peek(), Some(Token::RParen)) {
683                return Err("expected ',' or ')' in call".to_string());
684            }
685        }
686
687        self.expect(Token::RParen)?;
688        Ok(Expr::Call(CallExpr {
689            name,
690            positional,
691            keyword,
692        }))
693    }
694
695    fn expect(&mut self, expected: Token) -> Result<(), String> {
696        match self.next() {
697            Some(token) if *token == expected => Ok(()),
698            Some(token) => Err(format!("expected {:?}, found {:?}", expected, token)),
699            None => Err(format!("expected {:?}, found end of input", expected)),
700        }
701    }
702
703    fn expect_end(&self) -> Result<(), String> {
704        if self.index == self.tokens.len() {
705            Ok(())
706        } else {
707            Err(format!(
708                "unexpected trailing tokens: {:?}",
709                &self.tokens[self.index..]
710            ))
711        }
712    }
713
714    fn peek(&self) -> Option<&'a Token> {
715        self.tokens.get(self.index)
716    }
717
718    fn peek_n(&self, offset: usize) -> Option<&'a Token> {
719        self.tokens.get(self.index + offset)
720    }
721
722    fn next(&mut self) -> Option<&'a Token> {
723        let token = self.tokens.get(self.index);
724        if token.is_some() {
725            self.index += 1;
726        }
727        token
728    }
729}
730
731fn expr_as_string(expr: &Expr) -> Option<&str> {
732    match expr {
733        Expr::String(value) => Some(value.as_str()),
734        _ => None,
735    }
736}
737
738fn expr_as_bool(expr: &Expr) -> Option<bool> {
739    match expr {
740        Expr::Bool(value) => Some(*value),
741        _ => None,
742    }
743}
744
745fn extract_string_values(expr: &Expr) -> Vec<String> {
746    match expr {
747        Expr::String(value) => vec![value.clone()],
748        Expr::Array(values) => values
749            .iter()
750            .filter_map(expr_as_string)
751            .map(ToOwned::to_owned)
752            .collect(),
753        _ => Vec::new(),
754    }
755}