1use 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}