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 .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}