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};
12use crate::parsers::utils::{
13 MAX_ITERATION_COUNT, RecursionGuard, capped_iteration_limit, read_file_to_string,
14 truncate_field,
15};
16
17use super::PackageParser;
18use super::metadata::ParserMetadata;
19
20pub struct NixFlakeLockParser;
21
22impl PackageParser for NixFlakeLockParser {
23 const PACKAGE_TYPE: PackageType = PackageType::Nix;
24
25 fn metadata() -> Vec<ParserMetadata> {
26 vec![ParserMetadata {
27 description: "Nix flake lockfile",
28 file_patterns: &["**/flake.lock"],
29 package_type: "nix",
30 primary_language: "JSON",
31 documentation_url: Some(
32 "https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-flake.html",
33 ),
34 }]
35 }
36
37 fn is_match(path: &Path) -> bool {
38 path.file_name().is_some_and(|name| name == "flake.lock")
39 }
40
41 fn extract_packages(path: &Path) -> Vec<PackageData> {
42 let content = match read_file_to_string(path, None) {
43 Ok(content) => content,
44 Err(error) => {
45 warn!("Failed to read flake.lock at {:?}: {}", path, error);
46 return vec![default_flake_lock_package_data()];
47 }
48 };
49
50 let json: JsonValue = match serde_json::from_str(&content) {
51 Ok(json) => json,
52 Err(error) => {
53 warn!("Failed to parse flake.lock at {:?}: {}", path, error);
54 return vec![default_flake_lock_package_data()];
55 }
56 };
57
58 match parse_flake_lock(path, &json) {
59 Ok(package) => vec![package],
60 Err(error) => {
61 warn!("Failed to interpret flake.lock at {:?}: {}", path, error);
62 vec![default_flake_lock_package_data()]
63 }
64 }
65 }
66}
67
68pub struct NixFlakeParser;
69
70impl PackageParser for NixFlakeParser {
71 const PACKAGE_TYPE: PackageType = PackageType::Nix;
72
73 fn metadata() -> Vec<ParserMetadata> {
74 vec![ParserMetadata {
75 description: "Nix flake manifest",
76 file_patterns: &["**/flake.nix"],
77 package_type: "nix",
78 primary_language: "Nix",
79 documentation_url: Some(
80 "https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-flake.html",
81 ),
82 }]
83 }
84
85 fn is_match(path: &Path) -> bool {
86 path.file_name().is_some_and(|name| name == "flake.nix")
87 }
88
89 fn extract_packages(path: &Path) -> Vec<PackageData> {
90 let content = match read_file_to_string(path, None) {
91 Ok(content) => content,
92 Err(error) => {
93 warn!("Failed to read flake.nix at {:?}: {}", path, error);
94 return vec![default_flake_package_data()];
95 }
96 };
97
98 match parse_flake_nix(path, &content) {
99 Ok(package) => vec![package],
100 Err(_) => vec![default_flake_package_data()],
101 }
102 }
103}
104
105pub struct NixDefaultParser;
106
107impl PackageParser for NixDefaultParser {
108 const PACKAGE_TYPE: PackageType = PackageType::Nix;
109
110 fn metadata() -> Vec<ParserMetadata> {
111 vec![ParserMetadata {
112 description: "Nix derivation manifest",
113 file_patterns: &["**/default.nix"],
114 package_type: "nix",
115 primary_language: "Nix",
116 documentation_url: Some("https://nix.dev/manual/nix/stable/language/derivations.html"),
117 }]
118 }
119
120 fn is_match(path: &Path) -> bool {
121 path.file_name().is_some_and(|name| name == "default.nix")
122 }
123
124 fn extract_packages(path: &Path) -> Vec<PackageData> {
125 let content = match read_file_to_string(path, None) {
126 Ok(content) => content,
127 Err(error) => {
128 warn!("Failed to read default.nix at {:?}: {}", path, error);
129 return vec![default_default_nix_package_data()];
130 }
131 };
132
133 match parse_default_nix(path, &content) {
134 Ok(package) => vec![package],
135 Err(_) => vec![default_default_nix_package_data()],
136 }
137 }
138}
139
140#[derive(Clone, Debug)]
141enum Expr {
142 AttrSet(Vec<(Vec<String>, Expr)>),
143 List(Vec<Expr>),
144 String(String),
145 Symbol(String),
146 Application(Vec<Expr>),
147 Let {
148 bindings: Vec<(Vec<String>, Expr)>,
149 body: Box<Expr>,
150 },
151 Select {
152 target: Box<Expr>,
153 path: Vec<String>,
154 },
155}
156
157type NixAttrEntries = [(Vec<String>, Expr)];
158type NixAttrEntriesRef<'a> = &'a NixAttrEntries;
159type NixScopeStack<'a> = Vec<NixAttrEntriesRef<'a>>;
160
161#[derive(Clone, Debug, PartialEq, Eq)]
162enum Token {
163 LBrace,
164 RBrace,
165 LBracket,
166 RBracket,
167 LParen,
168 RParen,
169 Equals,
170 Semicolon,
171 Colon,
172 Dot,
173 Comma,
174 String(String),
175 Ident(String),
176}
177
178#[derive(Default)]
179struct FlakeInputInfo {
180 requirement: Option<String>,
181 follows: Vec<String>,
182 flake: Option<bool>,
183}
184
185struct Lexer {
186 chars: Vec<char>,
187 index: usize,
188}
189
190impl Lexer {
191 fn new(input: &str) -> Self {
192 Self {
193 chars: input.chars().collect(),
194 index: 0,
195 }
196 }
197
198 fn tokenize(mut self) -> Result<Vec<Token>, String> {
199 let mut tokens = Vec::new();
200
201 while let Some(ch) = self.peek() {
202 if tokens.len() >= MAX_ITERATION_COUNT {
203 warn!("Lexer exceeded MAX_ITERATION_COUNT token limit");
204 break;
205 }
206
207 if ch.is_whitespace() {
208 self.index += 1;
209 continue;
210 }
211
212 if ch == '#' {
213 self.skip_line_comment();
214 continue;
215 }
216
217 if ch == '/' && self.peek_n(1) == Some('*') {
218 self.skip_block_comment()?;
219 continue;
220 }
221
222 match ch {
223 '$' if self.peek_n(1) == Some('{') => {
224 tokens.push(Token::Ident(self.read_interpolation_literal()?));
225 }
226 '.' if self.peek_n(1) == Some('/') => {
227 tokens.push(Token::Ident(self.read_path_literal()?));
228 }
229 '.' if self.peek_n(1) == Some('.') && self.peek_n(2) == Some('/') => {
230 tokens.push(Token::Ident(self.read_path_literal()?));
231 }
232 '{' => {
233 self.index += 1;
234 tokens.push(Token::LBrace);
235 }
236 '}' => {
237 self.index += 1;
238 tokens.push(Token::RBrace);
239 }
240 '[' => {
241 self.index += 1;
242 tokens.push(Token::LBracket);
243 }
244 ']' => {
245 self.index += 1;
246 tokens.push(Token::RBracket);
247 }
248 '(' => {
249 self.index += 1;
250 tokens.push(Token::LParen);
251 }
252 ')' => {
253 self.index += 1;
254 tokens.push(Token::RParen);
255 }
256 '=' => {
257 self.index += 1;
258 tokens.push(Token::Equals);
259 }
260 ';' => {
261 self.index += 1;
262 tokens.push(Token::Semicolon);
263 }
264 ':' => {
265 self.index += 1;
266 tokens.push(Token::Colon);
267 }
268 '.' => {
269 self.index += 1;
270 tokens.push(Token::Dot);
271 }
272 ',' => {
273 self.index += 1;
274 tokens.push(Token::Comma);
275 }
276 '"' => tokens.push(Token::String(self.read_double_quoted_string()?)),
277 '\'' if self.peek_n(1) == Some('\'') => {
278 tokens.push(Token::String(self.read_indented_string()?));
279 }
280 _ => tokens.push(Token::Ident(self.read_ident()?)),
281 }
282 }
283
284 Ok(tokens)
285 }
286
287 fn peek(&self) -> Option<char> {
288 self.chars.get(self.index).copied()
289 }
290
291 fn peek_n(&self, offset: usize) -> Option<char> {
292 self.chars.get(self.index + offset).copied()
293 }
294
295 fn skip_line_comment(&mut self) {
296 while let Some(ch) = self.peek() {
297 self.index += 1;
298 if ch == '\n' {
299 break;
300 }
301 }
302 }
303
304 fn skip_block_comment(&mut self) -> Result<(), String> {
305 self.index += 2;
306 while let Some(ch) = self.peek() {
307 if ch == '*' && self.peek_n(1) == Some('/') {
308 self.index += 2;
309 return Ok(());
310 }
311 self.index += 1;
312 }
313 Err("unterminated block comment".to_string())
314 }
315
316 fn read_double_quoted_string(&mut self) -> Result<String, String> {
317 self.index += 1;
318 let mut result = String::new();
319 let mut escaped = false;
320
321 while let Some(ch) = self.peek() {
322 self.index += 1;
323 if escaped {
324 result.push(match ch {
325 'n' => '\n',
326 'r' => '\r',
327 't' => '\t',
328 '"' => '"',
329 '\\' => '\\',
330 other => other,
331 });
332 escaped = false;
333 continue;
334 }
335
336 if ch == '\\' {
337 escaped = true;
338 continue;
339 }
340
341 if ch == '$' && self.peek() == Some('{') {
342 result.push(ch);
343 result.push('{');
344 self.index += 1;
345 let mut interpolation_depth = 1usize;
346
347 while let Some(inner) = self.peek() {
348 self.index += 1;
349 result.push(inner);
350
351 match inner {
352 '{' => interpolation_depth += 1,
353 '}' => {
354 interpolation_depth = interpolation_depth.saturating_sub(1);
355 if interpolation_depth == 0 {
356 break;
357 }
358 }
359 _ => {}
360 }
361 }
362
363 if interpolation_depth != 0 {
364 return Err("unterminated string interpolation".to_string());
365 }
366
367 continue;
368 }
369
370 if ch == '"' {
371 return Ok(result);
372 }
373
374 result.push(ch);
375 }
376
377 Err("unterminated string".to_string())
378 }
379
380 fn read_path_literal(&mut self) -> Result<String, String> {
381 let start = self.index;
382
383 while let Some(ch) = self.peek() {
384 if ch.is_whitespace()
385 || matches!(
386 ch,
387 '{' | '}' | '[' | ']' | '(' | ')' | '=' | ';' | ':' | ',' | '"'
388 )
389 || (ch == '\'' && self.peek_n(1) == Some('\''))
390 || ch == '#'
391 {
392 break;
393 }
394
395 if ch == '/' && self.peek_n(1) == Some('*') {
396 break;
397 }
398
399 self.index += 1;
400 }
401
402 if self.index == start {
403 return Err("unexpected token".to_string());
404 }
405
406 Ok(self.chars[start..self.index].iter().collect())
407 }
408
409 fn read_interpolation_literal(&mut self) -> Result<String, String> {
410 let start = self.index;
411 self.index += 2;
412 let mut depth = 1usize;
413
414 while let Some(ch) = self.peek() {
415 self.index += 1;
416
417 match ch {
418 '{' => depth += 1,
419 '}' => {
420 depth = depth.saturating_sub(1);
421 if depth == 0 {
422 return Ok(self.chars[start..self.index].iter().collect());
423 }
424 }
425 _ => {}
426 }
427 }
428
429 Err("unterminated interpolation literal".to_string())
430 }
431
432 fn read_indented_string(&mut self) -> Result<String, String> {
433 self.index += 2;
434 let mut result = String::new();
435
436 while let Some(ch) = self.peek() {
437 if ch == '\'' && self.peek_n(1) == Some('\'') {
438 self.index += 2;
439 return Ok(result);
440 }
441 result.push(ch);
442 self.index += 1;
443 }
444
445 Err("unterminated indented string".to_string())
446 }
447
448 fn read_ident(&mut self) -> Result<String, String> {
449 let start = self.index;
450
451 while let Some(ch) = self.peek() {
452 if ch.is_whitespace()
453 || matches!(
454 ch,
455 '{' | '}' | '[' | ']' | '(' | ')' | '=' | ';' | ':' | ',' | '.' | '"'
456 )
457 || (ch == '\'' && self.peek_n(1) == Some('\''))
458 || ch == '#'
459 {
460 break;
461 }
462
463 if ch == '/' && self.peek_n(1) == Some('*') {
464 break;
465 }
466
467 self.index += 1;
468 }
469
470 if self.index == start {
471 return Err("unexpected token".to_string());
472 }
473
474 Ok(self.chars[start..self.index].iter().collect())
475 }
476}
477
478struct Parser {
479 tokens: Vec<Token>,
480 index: usize,
481 guard: RecursionGuard<()>,
482}
483
484impl Parser {
485 fn new(tokens: Vec<Token>) -> Self {
486 Self {
487 tokens,
488 index: 0,
489 guard: RecursionGuard::depth_only(),
490 }
491 }
492
493 fn parse(mut self) -> Result<Expr, String> {
494 let expr = self.parse_expr()?;
495 if self.peek().is_some() {
496 return Err("unexpected trailing tokens".to_string());
497 }
498 Ok(expr)
499 }
500
501 fn parse_expr(&mut self) -> Result<Expr, String> {
502 if self.guard.descend() {
503 return Err("recursion depth exceeded".to_string());
504 }
505
506 if self.peek() == Some(&Token::LBrace) && self.looks_like_lambda_binder_set()? {
507 self.skip_lambda_binder_set()?;
508 self.expect(&Token::Colon)?;
509 let result = self.parse_expr();
510 self.guard.ascend();
511 return result;
512 }
513
514 if self.looks_like_prefixed_lambda_binder_set()? {
515 self.index += 1;
516 self.skip_lambda_binder_set()?;
517 self.expect(&Token::Colon)?;
518 let result = self.parse_expr();
519 self.guard.ascend();
520 return result;
521 }
522
523 let first = self.parse_term()?;
524 if self.consume(&Token::Colon) {
525 let result = self.parse_expr();
526 self.guard.ascend();
527 return result;
528 }
529
530 let mut terms = vec![first];
531 while self.can_start_term() {
532 terms.push(self.parse_term()?);
533 }
534
535 let expr = if terms.len() == 1 {
536 terms
537 .into_iter()
538 .next()
539 .unwrap_or_else(|| Expr::Symbol(String::new()))
540 } else {
541 Expr::Application(terms)
542 };
543
544 let result = self.parse_postfix(expr);
545 self.guard.ascend();
546 result
547 }
548
549 fn parse_postfix(&mut self, mut expr: Expr) -> Result<Expr, String> {
550 while self.consume(&Token::Dot) {
551 let mut path = vec![self.take_attr_key()?];
552 while self.consume(&Token::Dot) {
553 path.push(self.take_attr_key()?);
554 }
555 expr = Expr::Select {
556 target: Box::new(expr),
557 path,
558 };
559 }
560
561 Ok(expr)
562 }
563
564 fn parse_term(&mut self) -> Result<Expr, String> {
565 match self.peek() {
566 Some(Token::Ident(keyword)) if keyword == "let" => self.parse_let_in_expr(),
567 Some(Token::Ident(keyword)) if keyword == "with" => {
568 self.index += 1;
569 let _ = self.parse_expr()?;
570 self.expect(&Token::Semicolon)?;
571 self.parse_expr()
572 }
573 Some(Token::Ident(keyword)) if keyword == "rec" => {
574 if matches!(self.peek_n(1), Some(Token::LBrace)) {
575 self.index += 1;
576 self.parse_attrset()
577 } else {
578 self.parse_symbol()
579 }
580 }
581 Some(Token::LBrace) => self.parse_attrset(),
582 Some(Token::LBracket) => self.parse_list(),
583 Some(Token::LParen) => {
584 self.index += 1;
585 let expr = self.parse_expr()?;
586 self.expect(&Token::RParen)?;
587 Ok(expr)
588 }
589 Some(Token::String(_)) => self.parse_string(),
590 Some(Token::Ident(_)) => self.parse_symbol(),
591 _ => Err("expected expression".to_string()),
592 }
593 }
594
595 fn parse_let_in_expr(&mut self) -> Result<Expr, String> {
596 self.take_exact_ident("let")?;
597 let mut bindings = Vec::new();
598
599 while !matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "in") {
600 if self.peek().is_none() {
601 return Err("unterminated let expression".to_string());
602 }
603
604 if bindings.len() >= MAX_ITERATION_COUNT {
605 warn!("parse_let_in_expr exceeded MAX_ITERATION_COUNT bindings limit");
606 break;
607 }
608
609 if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
610 bindings.extend(self.parse_inherit_entries()?);
611 continue;
612 }
613
614 let key = self.parse_attr_path()?;
615 self.expect(&Token::Equals)?;
616 let value = self.parse_expr()?;
617 self.expect(&Token::Semicolon)?;
618 bindings.push((key, value));
619 }
620
621 self.take_exact_ident("in")?;
622 let body = self.parse_expr()?;
623 Ok(Expr::Let {
624 bindings,
625 body: Box::new(body),
626 })
627 }
628
629 fn parse_attrset(&mut self) -> Result<Expr, String> {
630 self.expect(&Token::LBrace)?;
631 let mut entries = Vec::new();
632
633 loop {
634 if self.consume(&Token::RBrace) {
635 return Ok(Expr::AttrSet(entries));
636 }
637
638 if self.peek().is_none() {
639 return Err("unterminated attribute set".to_string());
640 }
641
642 if entries.len() >= MAX_ITERATION_COUNT {
643 warn!("parse_attrset exceeded MAX_ITERATION_COUNT entries limit");
644 break;
645 }
646
647 if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
648 entries.extend(self.parse_inherit_entries()?);
649 continue;
650 }
651
652 let key = self.parse_attr_path()?;
653 self.expect(&Token::Equals)?;
654 let value = self.parse_expr()?;
655 self.expect(&Token::Semicolon)?;
656 entries.push((key, value));
657 }
658
659 Ok(Expr::AttrSet(entries))
660 }
661
662 fn parse_attr_path(&mut self) -> Result<Vec<String>, String> {
663 let mut path = vec![self.take_attr_key()?];
664 while self.consume(&Token::Dot) {
665 path.push(self.take_attr_key()?);
666 }
667 Ok(path)
668 }
669
670 fn parse_inherit_entries(&mut self) -> Result<Vec<(Vec<String>, Expr)>, String> {
671 self.take_exact_ident("inherit")?;
672
673 let inherit_from = if self.consume(&Token::LParen) {
674 let expr = self.parse_expr()?;
675 self.expect(&Token::RParen)?;
676 Some(expr)
677 } else {
678 None
679 };
680
681 let mut entries = Vec::new();
682 while !self.consume(&Token::Semicolon) {
683 if self.peek().is_none() {
684 return Err("unterminated inherit statement".to_string());
685 }
686
687 if entries.len() >= MAX_ITERATION_COUNT {
688 warn!("parse_inherit_entries exceeded MAX_ITERATION_COUNT entries limit");
689 break;
690 }
691
692 let name = self.take_attr_key()?;
693 let value = match &inherit_from {
694 Some(source) => Expr::Select {
695 target: Box::new(source.clone()),
696 path: vec![name.clone()],
697 },
698 None => Expr::Symbol(name.clone()),
699 };
700 entries.push((vec![name], value));
701 }
702
703 Ok(entries)
704 }
705
706 fn parse_list(&mut self) -> Result<Expr, String> {
707 self.expect(&Token::LBracket)?;
708 let mut items = Vec::new();
709 while !self.consume(&Token::RBracket) {
710 if self.peek().is_none() {
711 return Err("unterminated list".to_string());
712 }
713
714 if items.len() >= MAX_ITERATION_COUNT {
715 warn!("parse_list exceeded MAX_ITERATION_COUNT items limit");
716 break;
717 }
718
719 items.push(self.parse_expr()?);
720 }
721 Ok(Expr::List(items))
722 }
723
724 fn parse_string(&mut self) -> Result<Expr, String> {
725 match self.next() {
726 Some(Token::String(value)) => Ok(Expr::String(value)),
727 _ => Err("expected string".to_string()),
728 }
729 }
730
731 fn parse_symbol(&mut self) -> Result<Expr, String> {
732 let mut parts = vec![self.take_ident()?];
733 while self.consume(&Token::Dot) {
734 parts.push(self.take_ident()?);
735 }
736 Ok(Expr::Symbol(parts.join(".")))
737 }
738
739 fn take_ident(&mut self) -> Result<String, String> {
740 match self.next() {
741 Some(Token::Ident(value)) => Ok(value),
742 _ => Err("expected identifier".to_string()),
743 }
744 }
745
746 fn take_exact_ident(&mut self, expected: &str) -> Result<(), String> {
747 match self.next() {
748 Some(Token::Ident(value)) if value == expected => Ok(()),
749 _ => Err(format!("expected {expected}")),
750 }
751 }
752
753 fn take_attr_key(&mut self) -> Result<String, String> {
754 match self.next() {
755 Some(Token::Ident(value)) | Some(Token::String(value)) => Ok(value),
756 _ => Err("expected attribute key".to_string()),
757 }
758 }
759
760 fn can_start_term(&self) -> bool {
761 matches!(
762 self.peek(),
763 Some(Token::LBrace)
764 | Some(Token::LBracket)
765 | Some(Token::LParen)
766 | Some(Token::String(_))
767 | Some(Token::Ident(_))
768 )
769 }
770
771 fn looks_like_lambda_binder_set(&self) -> Result<bool, String> {
772 if self.peek() != Some(&Token::LBrace) {
773 return Ok(false);
774 }
775
776 self.looks_like_lambda_binder_set_from(self.index)
777 }
778
779 fn looks_like_prefixed_lambda_binder_set(&self) -> Result<bool, String> {
780 match (self.peek(), self.peek_n(1)) {
781 (Some(Token::Ident(prefix)), Some(Token::LBrace)) if prefix.ends_with('@') => {
782 self.looks_like_lambda_binder_set_from(self.index + 1)
783 }
784 _ => Ok(false),
785 }
786 }
787
788 fn looks_like_lambda_binder_set_from(&self, start_index: usize) -> Result<bool, String> {
789 if self.tokens.get(start_index) != Some(&Token::LBrace) {
790 return Ok(false);
791 }
792
793 let mut depth = 0usize;
794 let mut index = start_index;
795
796 while let Some(token) = self.tokens.get(index) {
797 match token {
798 Token::LBrace => depth += 1,
799 Token::RBrace => {
800 depth = depth.saturating_sub(1);
801 if depth == 0 {
802 return Ok(matches!(self.tokens.get(index + 1), Some(Token::Colon)));
803 }
804 }
805 Token::Equals | Token::Semicolon if depth == 1 => return Ok(false),
806 _ => {}
807 }
808
809 index += 1;
810 }
811
812 Err("unterminated lambda binder set".to_string())
813 }
814
815 fn skip_lambda_binder_set(&mut self) -> Result<(), String> {
816 self.expect(&Token::LBrace)?;
817 let mut depth = 1usize;
818
819 while depth > 0 {
820 match self.next() {
821 Some(Token::LBrace) => depth += 1,
822 Some(Token::RBrace) => depth = depth.saturating_sub(1),
823 Some(_) => {}
824 None => return Err("unterminated lambda binder set".to_string()),
825 }
826 }
827
828 Ok(())
829 }
830
831 fn expect(&mut self, expected: &Token) -> Result<(), String> {
832 if self.consume(expected) {
833 Ok(())
834 } else {
835 Err(format!("expected {:?}", expected))
836 }
837 }
838
839 fn consume(&mut self, expected: &Token) -> bool {
840 if self.peek() == Some(expected) {
841 self.index += 1;
842 true
843 } else {
844 false
845 }
846 }
847
848 fn peek(&self) -> Option<&Token> {
849 self.tokens.get(self.index)
850 }
851
852 fn peek_n(&self, offset: usize) -> Option<&Token> {
853 self.tokens.get(self.index + offset)
854 }
855
856 fn next(&mut self) -> Option<Token> {
857 let token = self.tokens.get(self.index).cloned();
858 if token.is_some() {
859 self.index += 1;
860 }
861 token
862 }
863}
864
865fn parse_flake_nix(path: &Path, content: &str) -> Result<PackageData, String> {
866 let expr = parse_nix_expr(content)?;
867 let scopes = Vec::new();
868 let (root, scopes) =
869 root_attrset_with_scopes(&expr, &scopes, &mut RecursionGuard::depth_only())
870 .ok_or_else(|| "flake.nix root was not an attribute set".to_string())?;
871
872 let mut package = default_flake_package_data();
873 package.name = fallback_name(path).map(truncate_field);
874 package.description =
875 find_string_attr_with_scopes(root, &["description"], &scopes).map(truncate_field);
876 package.purl = package
877 .name
878 .as_deref()
879 .and_then(|name| build_nix_purl(name, None));
880 package.dependencies = build_flake_dependencies(root, &scopes);
881
882 Ok(package)
883}
884
885fn parse_default_nix(path: &Path, content: &str) -> Result<PackageData, String> {
886 match parse_nix_expr(content) {
887 Ok(expr) => extract_default_nix_package(path, &expr, &Vec::new(), 0)
888 .or_else(|_| extract_flake_compat_default_package_from_content(path, content)),
889 Err(parse_error) => extract_flake_compat_default_package_from_content(path, content)
890 .map_err(|_| parse_error),
891 }
892}
893
894fn parse_flake_lock(path: &Path, json: &JsonValue) -> Result<PackageData, String> {
895 let version = json
896 .get("version")
897 .and_then(JsonValue::as_i64)
898 .ok_or_else(|| "flake.lock missing integer version".to_string())?;
899 let root = json
900 .get("root")
901 .and_then(JsonValue::as_str)
902 .ok_or_else(|| "flake.lock missing root".to_string())?;
903 let nodes = json
904 .get("nodes")
905 .and_then(JsonValue::as_object)
906 .ok_or_else(|| "flake.lock missing nodes".to_string())?;
907 let root_node = nodes
908 .get(root)
909 .and_then(JsonValue::as_object)
910 .ok_or_else(|| "flake.lock root node missing".to_string())?;
911 let root_inputs = root_node
912 .get("inputs")
913 .and_then(JsonValue::as_object)
914 .ok_or_else(|| "flake.lock root node missing inputs".to_string())?;
915
916 let mut package = default_flake_lock_package_data();
917 package.name = fallback_name(path).map(truncate_field);
918 package.purl = package
919 .name
920 .as_deref()
921 .and_then(|name| build_nix_purl(name, None));
922
923 let mut extra_data = HashMap::new();
924 extra_data.insert("lock_version".to_string(), JsonValue::from(version));
925 extra_data.insert("root".to_string(), JsonValue::String(root.to_string()));
926 package.extra_data = Some(extra_data);
927
928 let root_inputs_limit =
929 capped_iteration_limit(root_inputs.len(), "nix: flake.lock root inputs");
930 package.dependencies = root_inputs
931 .iter()
932 .take(root_inputs_limit)
933 .filter_map(|(input_name, node_ref)| build_lock_dependency(input_name, node_ref, nodes))
934 .collect();
935 package
936 .dependencies
937 .sort_by(|left, right| left.purl.cmp(&right.purl));
938
939 Ok(package)
940}
941
942fn build_lock_dependency(
943 input_name: &str,
944 node_ref: &JsonValue,
945 nodes: &serde_json::Map<String, JsonValue>,
946) -> Option<Dependency> {
947 let node_id = node_ref.as_str()?;
948 let node = nodes.get(node_id)?.as_object()?;
949 let locked = node.get("locked").and_then(JsonValue::as_object)?;
950 let revision = locked.get("rev").and_then(JsonValue::as_str);
951
952 let mut extra_data = HashMap::new();
953 for key in [
954 "type",
955 "owner",
956 "repo",
957 "narHash",
958 "lastModified",
959 "revCount",
960 "url",
961 "path",
962 "dir",
963 "host",
964 ] {
965 if let Some(value) = locked.get(key) {
966 extra_data.insert(normalize_extra_key(key), value.clone());
967 }
968 }
969 if let Some(value) = node.get("flake").and_then(JsonValue::as_bool) {
970 extra_data.insert("flake".to_string(), JsonValue::Bool(value));
971 }
972 if let Some(original) = node.get("original").and_then(JsonValue::as_object) {
973 if let Some(value) = original.get("type") {
974 extra_data.insert("original_type".to_string(), value.clone());
975 }
976 if let Some(value) = original.get("id") {
977 extra_data.insert("original_id".to_string(), value.clone());
978 }
979 if let Some(value) = original.get("ref") {
980 extra_data.insert("original_ref".to_string(), value.clone());
981 }
982 }
983
984 Some(Dependency {
985 purl: build_nix_purl(input_name, revision),
986 extracted_requirement: build_locked_requirement(locked, node.get("original"))
987 .map(truncate_field),
988 scope: Some("inputs".to_string()),
989 is_runtime: Some(false),
990 is_optional: Some(false),
991 is_pinned: Some(revision.is_some()),
992 is_direct: Some(true),
993 resolved_package: None,
994 extra_data: (!extra_data.is_empty()).then_some(extra_data),
995 })
996}
997
998fn build_locked_requirement(
999 locked: &serde_json::Map<String, JsonValue>,
1000 original: Option<&JsonValue>,
1001) -> Option<String> {
1002 let source_type = locked.get("type").and_then(JsonValue::as_str).or_else(|| {
1003 original
1004 .and_then(|value| value.get("type"))
1005 .and_then(JsonValue::as_str)
1006 });
1007
1008 match source_type {
1009 Some("github") => {
1010 let owner = locked.get("owner").and_then(JsonValue::as_str)?;
1011 let repo = locked.get("repo").and_then(JsonValue::as_str)?;
1012 Some(format!("github:{owner}/{repo}"))
1013 }
1014 Some("indirect") => original
1015 .and_then(|value| value.get("id"))
1016 .and_then(JsonValue::as_str)
1017 .map(ToOwned::to_owned),
1018 _ => locked
1019 .get("url")
1020 .and_then(JsonValue::as_str)
1021 .map(ToOwned::to_owned),
1022 }
1023}
1024
1025fn normalize_extra_key(key: &str) -> String {
1026 match key {
1027 "type" => "source_type".to_string(),
1028 "narHash" => "nar_hash".to_string(),
1029 "lastModified" => "last_modified".to_string(),
1030 "revCount" => "rev_count".to_string(),
1031 other => other.to_string(),
1032 }
1033}
1034
1035fn build_flake_dependencies(
1036 root: &[(Vec<String>, Expr)],
1037 scopes: &[&[(Vec<String>, Expr)]],
1038) -> Vec<Dependency> {
1039 let mut inputs: HashMap<String, FlakeInputInfo> = HashMap::new();
1040
1041 for (path, expr) in root {
1042 if path.first().map(String::as_str) != Some("inputs") {
1043 continue;
1044 }
1045
1046 if path.len() == 1 {
1047 if let Some(entries) = attrset_entries(expr) {
1048 collect_input_entries(entries, scopes, &mut inputs, None);
1049 }
1050 continue;
1051 }
1052
1053 collect_input_path(&path[1..], expr, scopes, &mut inputs);
1054 }
1055
1056 let mut dependencies = inputs
1057 .into_iter()
1058 .map(|(name, info)| {
1059 let mut extra_data = HashMap::new();
1060 if info.follows.len() == 1 {
1061 extra_data.insert(
1062 "follows".to_string(),
1063 JsonValue::String(info.follows[0].clone()),
1064 );
1065 } else if !info.follows.is_empty() {
1066 extra_data.insert(
1067 "follows".to_string(),
1068 JsonValue::Array(
1069 info.follows
1070 .iter()
1071 .cloned()
1072 .map(JsonValue::String)
1073 .collect(),
1074 ),
1075 );
1076 }
1077 if let Some(flake) = info.flake {
1078 extra_data.insert("flake".to_string(), JsonValue::Bool(flake));
1079 }
1080
1081 Dependency {
1082 purl: build_nix_purl(&name, None),
1083 extracted_requirement: info.requirement.map(truncate_field),
1084 scope: Some("inputs".to_string()),
1085 is_runtime: Some(false),
1086 is_optional: Some(false),
1087 is_pinned: Some(false),
1088 is_direct: Some(true),
1089 resolved_package: None,
1090 extra_data: (!extra_data.is_empty()).then_some(extra_data),
1091 }
1092 })
1093 .collect::<Vec<_>>();
1094
1095 dependencies.sort_by(|left, right| left.purl.cmp(&right.purl));
1096 dependencies
1097}
1098
1099fn collect_input_entries(
1100 entries: &[(Vec<String>, Expr)],
1101 scopes: &[&[(Vec<String>, Expr)]],
1102 inputs: &mut HashMap<String, FlakeInputInfo>,
1103 current_input: Option<&str>,
1104) {
1105 for (path, expr) in entries {
1106 if let Some(input_name) = current_input {
1107 apply_input_field(
1108 inputs.entry(input_name.to_string()).or_default(),
1109 path,
1110 expr,
1111 scopes,
1112 );
1113 continue;
1114 }
1115
1116 collect_input_path(path, expr, scopes, inputs);
1117 }
1118}
1119
1120fn collect_input_path(
1121 path: &[String],
1122 expr: &Expr,
1123 scopes: &[&[(Vec<String>, Expr)]],
1124 inputs: &mut HashMap<String, FlakeInputInfo>,
1125) {
1126 let Some(input_name) = path.first() else {
1127 return;
1128 };
1129
1130 if path.len() == 1 {
1131 match expr {
1132 Expr::AttrSet(entries) => {
1133 collect_input_entries(entries, scopes, inputs, Some(input_name))
1134 }
1135 Expr::String(value) => {
1136 inputs.entry(input_name.clone()).or_default().requirement = Some(value.clone())
1137 }
1138 Expr::Symbol(value) => {
1139 inputs.entry(input_name.clone()).or_default().requirement =
1140 expr_as_string_with_scopes(
1141 &Expr::Symbol(value.clone()),
1142 scopes,
1143 &mut RecursionGuard::depth_only(),
1144 )
1145 }
1146 _ => {}
1147 }
1148 return;
1149 }
1150
1151 apply_input_field(
1152 inputs.entry(input_name.clone()).or_default(),
1153 &path[1..],
1154 expr,
1155 scopes,
1156 );
1157}
1158
1159fn apply_input_field(
1160 info: &mut FlakeInputInfo,
1161 path: &[String],
1162 expr: &Expr,
1163 scopes: &[&[(Vec<String>, Expr)]],
1164) {
1165 if path == ["url"] {
1166 info.requirement =
1167 expr_as_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only());
1168 return;
1169 }
1170
1171 if path == ["flake"] {
1172 info.flake = expr_as_bool_with_scopes(expr, scopes, &mut RecursionGuard::depth_only());
1173 return;
1174 }
1175
1176 if path.len() == 3
1177 && path[0] == "inputs"
1178 && path[2] == "follows"
1179 && let Some(value) =
1180 expr_as_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1181 {
1182 info.follows.push(value);
1183 }
1184}
1185
1186fn build_list_dependencies(
1187 entries: &[(Vec<String>, Expr)],
1188 field_name: &str,
1189 runtime: bool,
1190 scopes: &[&[(Vec<String>, Expr)]],
1191) -> Vec<Dependency> {
1192 let Some(expr) = find_attr(entries, &[field_name], &mut RecursionGuard::depth_only()) else {
1193 return Vec::new();
1194 };
1195 let Some(items) = list_items_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1196 else {
1197 return Vec::new();
1198 };
1199
1200 let items_limit = capped_iteration_limit(items.len(), "nix: dependency list items");
1201 items
1202 .iter()
1203 .take(items_limit)
1204 .flat_map(|expr| {
1205 expr_to_dependency_symbols_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1206 })
1207 .filter_map(|symbol| {
1208 let name = symbol.rsplit('.').next()?.to_string();
1209 Some(Dependency {
1210 purl: build_nix_purl(&name, None),
1211 extracted_requirement: None,
1212 scope: Some(field_name.to_string()),
1213 is_runtime: Some(runtime),
1214 is_optional: Some(false),
1215 is_pinned: Some(false),
1216 is_direct: Some(true),
1217 resolved_package: None,
1218 extra_data: None,
1219 })
1220 })
1221 .collect()
1222}
1223
1224fn expr_to_dependency_symbols_with_scopes(
1225 expr: &Expr,
1226 scopes: &[&[(Vec<String>, Expr)]],
1227 guard: &mut RecursionGuard<()>,
1228) -> Vec<String> {
1229 if guard.descend() {
1230 warn!("expr_to_dependency_symbols_with_scopes exceeded MAX_RECURSION_DEPTH");
1231 return Vec::new();
1232 }
1233
1234 let result = match expr {
1235 Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only())
1236 .map(|resolved| expr_to_dependency_symbols_with_scopes(resolved, scopes, guard))
1237 .unwrap_or_else(|| vec![symbol.clone()]),
1238 Expr::Application(parts) => parts
1239 .iter()
1240 .filter_map(|expr| {
1241 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1242 })
1243 .collect(),
1244 Expr::Let { bindings, body } => {
1245 let scopes = extend_scopes(scopes, bindings);
1246 expr_to_dependency_symbols_with_scopes(body, &scopes, guard)
1247 }
1248 Expr::Select { .. } => {
1249 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1250 .into_iter()
1251 .collect()
1252 }
1253 _ => Vec::new(),
1254 };
1255 guard.ascend();
1256 result
1257}
1258
1259fn fallback_name(path: &Path) -> Option<String> {
1260 path.parent()
1261 .and_then(|parent| parent.file_name())
1262 .and_then(|name| name.to_str())
1263 .map(ToOwned::to_owned)
1264}
1265
1266fn build_nix_purl(name: &str, version: Option<&str>) -> Option<String> {
1267 let mut purl = PackageUrl::new(PackageType::Nix.as_str(), name).ok()?;
1268 if let Some(version) = version {
1269 purl.with_version(version).ok()?;
1270 }
1271 Some(truncate_field(purl.to_string()))
1272}
1273
1274fn parse_nix_expr(content: &str) -> Result<Expr, String> {
1275 let tokens = Lexer::new(content).tokenize()?;
1276 Parser::new(tokens).parse()
1277}
1278
1279fn attrset_entries(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1280 match expr {
1281 Expr::AttrSet(entries) => Some(entries),
1282 _ => None,
1283 }
1284}
1285
1286fn list_items_with_scopes<'a>(
1287 expr: &'a Expr,
1288 scopes: &[&'a [(Vec<String>, Expr)]],
1289 guard: &mut RecursionGuard<()>,
1290) -> Option<&'a [Expr]> {
1291 if guard.descend() {
1292 warn!("list_items_with_scopes exceeded MAX_RECURSION_DEPTH");
1293 return None;
1294 }
1295
1296 let result = match expr {
1297 Expr::List(items) => Some(items.as_slice()),
1298 Expr::Let { bindings, body } => {
1299 let scopes = extend_scopes(scopes, bindings);
1300 list_items_with_scopes(body, &scopes, guard)
1301 }
1302 Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only())
1303 .and_then(|resolved| list_items_with_scopes(resolved, scopes, guard)),
1304 Expr::Select { target, path } => {
1305 resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1306 .and_then(|resolved| list_items_with_scopes(resolved, scopes, guard))
1307 }
1308 _ => None,
1309 };
1310 guard.ascend();
1311 result
1312}
1313
1314fn expr_as_symbol(expr: &Expr) -> Option<String> {
1315 match expr {
1316 Expr::Symbol(value) => Some(value.clone()),
1317 _ => None,
1318 }
1319}
1320
1321fn expr_as_symbol_with_scopes(
1322 expr: &Expr,
1323 scopes: &[&[(Vec<String>, Expr)]],
1324 guard: &mut RecursionGuard<()>,
1325) -> Option<String> {
1326 if guard.descend() {
1327 warn!("expr_as_symbol_with_scopes exceeded MAX_RECURSION_DEPTH");
1328 return None;
1329 }
1330
1331 let result = match expr {
1332 Expr::Symbol(value) => resolve_symbol(value, scopes, &mut RecursionGuard::depth_only())
1333 .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes, guard))
1334 .or_else(|| Some(value.clone())),
1335 Expr::Select { target, path } => {
1336 resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1337 .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes, guard))
1338 }
1339 Expr::Let { bindings, body } => {
1340 let scopes = extend_scopes(scopes, bindings);
1341 expr_as_symbol_with_scopes(body, &scopes, guard)
1342 }
1343 _ => expr_as_symbol(expr),
1344 };
1345 guard.ascend();
1346 result
1347}
1348
1349fn expr_as_bool(expr: &Expr) -> Option<bool> {
1350 match expr {
1351 Expr::Symbol(value) if value == "true" => Some(true),
1352 Expr::Symbol(value) if value == "false" => Some(false),
1353 _ => None,
1354 }
1355}
1356
1357fn expr_as_bool_with_scopes(
1358 expr: &Expr,
1359 scopes: &[&[(Vec<String>, Expr)]],
1360 guard: &mut RecursionGuard<()>,
1361) -> Option<bool> {
1362 if guard.descend() {
1363 warn!("expr_as_bool_with_scopes exceeded MAX_RECURSION_DEPTH");
1364 return None;
1365 }
1366
1367 let result = match expr {
1368 Expr::Let { bindings, body } => {
1369 let scopes = extend_scopes(scopes, bindings);
1370 expr_as_bool_with_scopes(body, &scopes, guard)
1371 }
1372 Expr::Symbol(value) => resolve_symbol(value, scopes, &mut RecursionGuard::depth_only())
1373 .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes, guard))
1374 .or_else(|| expr_as_bool(expr)),
1375 Expr::Select { target, path } => {
1376 resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1377 .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes, guard))
1378 }
1379 _ => expr_as_bool(expr),
1380 };
1381 guard.ascend();
1382 result
1383}
1384
1385fn expr_as_string_with_scopes(
1386 expr: &Expr,
1387 scopes: &[&[(Vec<String>, Expr)]],
1388 guard: &mut RecursionGuard<()>,
1389) -> Option<String> {
1390 if guard.descend() {
1391 warn!("expr_as_string_with_scopes exceeded MAX_RECURSION_DEPTH");
1392 return None;
1393 }
1394
1395 let result = match expr {
1396 Expr::String(value) => Some(interpolate_string(value, scopes)),
1397 Expr::Symbol(value) => resolve_symbol(value, scopes, &mut RecursionGuard::depth_only())
1398 .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes, guard))
1399 .or_else(|| Some(value.clone())),
1400 Expr::Application(parts) => parts
1401 .last()
1402 .and_then(|expr| expr_as_string_with_scopes(expr, scopes, guard)),
1403 Expr::Let { bindings, body } => {
1404 let scopes = extend_scopes(scopes, bindings);
1405 expr_as_string_with_scopes(body, &scopes, guard)
1406 }
1407 Expr::Select { target, path } => {
1408 resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1409 .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes, guard))
1410 }
1411 _ => None,
1412 };
1413 guard.ascend();
1414 result
1415}
1416
1417fn expr_to_scalar_string_with_scopes(
1418 expr: &Expr,
1419 scopes: &[&[(Vec<String>, Expr)]],
1420 guard: &mut RecursionGuard<()>,
1421) -> Option<String> {
1422 if guard.descend() {
1423 warn!("expr_to_scalar_string_with_scopes exceeded MAX_RECURSION_DEPTH");
1424 return None;
1425 }
1426
1427 let result = match expr {
1428 Expr::Application(parts) => parts
1429 .last()
1430 .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes, guard)),
1431 _ => expr_as_string_with_scopes(expr, scopes, guard),
1432 };
1433 guard.ascend();
1434 result
1435}
1436
1437fn find_attr<'a>(
1438 entries: &'a [(Vec<String>, Expr)],
1439 path: &[&str],
1440 guard: &mut RecursionGuard<()>,
1441) -> Option<&'a Expr> {
1442 if guard.descend() {
1443 warn!("find_attr exceeded MAX_RECURSION_DEPTH");
1444 return None;
1445 }
1446
1447 let result = entries.iter().find_map(|(key, value)| {
1448 if key.iter().map(String::as_str).eq(path.iter().copied()) {
1449 return Some(value);
1450 }
1451
1452 if key.len() < path.len()
1453 && key
1454 .iter()
1455 .map(String::as_str)
1456 .eq(path[..key.len()].iter().copied())
1457 && let Expr::AttrSet(child_entries) = value
1458 && let Some(found) = find_attr(child_entries, &path[key.len()..], guard)
1459 {
1460 return Some(found);
1461 }
1462
1463 None
1464 });
1465
1466 guard.ascend();
1467 result
1468}
1469
1470fn find_string_attr_with_scopes(
1471 entries: &[(Vec<String>, Expr)],
1472 path: &[&str],
1473 scopes: &[&[(Vec<String>, Expr)]],
1474) -> Option<String> {
1475 find_attr(entries, path, &mut RecursionGuard::depth_only())
1476 .and_then(|expr| {
1477 expr_to_scalar_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1478 })
1479 .map(truncate_field)
1480}
1481
1482fn find_mk_derivation_attrset(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1483 match expr {
1484 Expr::Application(parts) => {
1485 let is_derivation = parts
1486 .first()
1487 .and_then(expr_as_symbol)
1488 .is_some_and(|symbol| symbol.ends_with("mkDerivation"));
1489 if is_derivation {
1490 return parts.iter().rev().find_map(attrset_entries);
1491 }
1492 None
1493 }
1494 _ => None,
1495 }
1496}
1497
1498fn extend_scopes<'a>(
1499 scopes: &[NixAttrEntriesRef<'a>],
1500 bindings: NixAttrEntriesRef<'a>,
1501) -> NixScopeStack<'a> {
1502 let mut extended = scopes.to_vec();
1503 extended.push(bindings);
1504 extended
1505}
1506
1507fn root_attrset_with_scopes<'a>(
1508 expr: &'a Expr,
1509 scopes: &[NixAttrEntriesRef<'a>],
1510 guard: &mut RecursionGuard<()>,
1511) -> Option<(NixAttrEntriesRef<'a>, NixScopeStack<'a>)> {
1512 if guard.descend() {
1513 warn!("root_attrset_with_scopes exceeded MAX_RECURSION_DEPTH");
1514 return None;
1515 }
1516
1517 let result = match expr {
1518 Expr::AttrSet(entries) => Some((entries.as_slice(), scopes.to_vec())),
1519 Expr::Let { bindings, body } => {
1520 let scopes = extend_scopes(scopes, bindings);
1521 root_attrset_with_scopes(body, &scopes, guard)
1522 }
1523 _ => None,
1524 };
1525 guard.ascend();
1526 result
1527}
1528
1529fn lookup_binding<'a>(scopes: &[NixAttrEntriesRef<'a>], name: &str) -> Option<&'a Expr> {
1530 scopes
1531 .iter()
1532 .rev()
1533 .find_map(|bindings| find_attr(bindings, &[name], &mut RecursionGuard::depth_only()))
1534}
1535
1536fn resolve_symbol<'a>(
1537 symbol: &str,
1538 scopes: &[NixAttrEntriesRef<'a>],
1539 guard: &mut RecursionGuard<()>,
1540) -> Option<&'a Expr> {
1541 if guard.descend() {
1542 return None;
1543 }
1544
1545 let mut parts = symbol.split('.');
1546 let head = parts.next()?;
1547 let mut expr = lookup_binding(scopes, head)?;
1548 let rest = parts.collect::<Vec<_>>();
1549 if rest.is_empty() {
1550 guard.ascend();
1551 return Some(expr);
1552 }
1553
1554 for segment in rest {
1555 expr = resolve_select(expr, &[segment.to_string()], scopes, guard)?;
1556 }
1557
1558 guard.ascend();
1559 Some(expr)
1560}
1561
1562fn resolve_select<'a>(
1563 target: &'a Expr,
1564 path: &[String],
1565 scopes: &[NixAttrEntriesRef<'a>],
1566 guard: &mut RecursionGuard<()>,
1567) -> Option<&'a Expr> {
1568 if guard.descend() {
1569 return None;
1570 }
1571
1572 let result = match target {
1573 Expr::AttrSet(entries) => find_attr(
1574 entries,
1575 &path.iter().map(String::as_str).collect::<Vec<_>>(),
1576 guard,
1577 ),
1578 Expr::Let { bindings, body } => {
1579 let scopes = extend_scopes(scopes, bindings);
1580 resolve_select(body, path, &scopes, guard)
1581 }
1582 Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, guard)
1583 .and_then(|resolved| resolve_select(resolved, path, scopes, guard)),
1584 Expr::Select {
1585 target: inner_target,
1586 path: inner_path,
1587 } => resolve_select(inner_target, inner_path, scopes, guard)
1588 .and_then(|resolved| resolve_select(resolved, path, scopes, guard)),
1589 _ => None,
1590 };
1591 guard.ascend();
1592 result
1593}
1594
1595fn interpolate_string(value: &str, scopes: &[&[(Vec<String>, Expr)]]) -> String {
1596 let mut result = String::new();
1597 let mut index = 0usize;
1598
1599 while let Some(relative_start) = value[index..].find("${") {
1600 let start = index + relative_start;
1601 result.push_str(&value[index..start]);
1602
1603 let placeholder_start = start + 2;
1604 let Some(relative_end) = value[placeholder_start..].find('}') else {
1605 result.push_str(&value[start..]);
1606 return result;
1607 };
1608 let end = placeholder_start + relative_end;
1609 let placeholder = value[placeholder_start..end].trim();
1610 if !placeholder.is_empty()
1611 && placeholder
1612 .chars()
1613 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
1614 && let Some(resolved) =
1615 resolve_symbol(placeholder, scopes, &mut RecursionGuard::depth_only())
1616 && let Some(replacement) =
1617 expr_as_string_with_scopes(resolved, scopes, &mut RecursionGuard::depth_only())
1618 {
1619 result.push_str(&replacement);
1620 } else {
1621 result.push_str(&value[start..=end]);
1622 }
1623
1624 index = end + 1;
1625 }
1626
1627 result.push_str(&value[index..]);
1628 result
1629}
1630
1631fn extract_default_nix_package(
1632 path: &Path,
1633 expr: &Expr,
1634 scopes: &[&[(Vec<String>, Expr)]],
1635 depth: usize,
1636) -> Result<PackageData, String> {
1637 if depth > 2 {
1638 return Err("default.nix exceeded supported local import depth".to_string());
1639 }
1640
1641 match expr {
1642 Expr::Let { bindings, body } => {
1643 let scopes = extend_scopes(scopes, bindings);
1644 extract_default_nix_package(path, body, &scopes, depth)
1645 }
1646 Expr::Application(parts) => {
1647 if let Some(derivation) = find_mk_derivation_attrset(expr) {
1648 return build_default_package_from_attrset(path, derivation, scopes);
1649 }
1650
1651 if let Some((imported_expr, imported_path)) =
1652 try_follow_local_nix_application(path, parts, scopes)
1653 {
1654 return extract_default_nix_package(
1655 &imported_path,
1656 &imported_expr,
1657 &Vec::new(),
1658 depth + 1,
1659 );
1660 }
1661
1662 if let Some(package) = parts
1663 .first()
1664 .and_then(|part| extract_flake_compat_package_from_expr(path, part, scopes, depth))
1665 {
1666 return Ok(package);
1667 }
1668
1669 Err("default.nix did not contain a supported mkDerivation call".to_string())
1670 }
1671 Expr::Select {
1672 target,
1673 path: select_path,
1674 } => {
1675 if let Some(package) =
1676 extract_flake_compat_package_from_select(path, target, select_path, scopes, depth)
1677 {
1678 return Ok(package);
1679 }
1680
1681 if let Some((imported_expr, imported_path)) =
1682 try_follow_selected_local_import(path, target, select_path, scopes)
1683 {
1684 return extract_default_nix_package(
1685 &imported_path,
1686 &imported_expr,
1687 &Vec::new(),
1688 depth + 1,
1689 );
1690 }
1691
1692 if let Some(resolved) = resolve_select(
1693 target,
1694 select_path,
1695 scopes,
1696 &mut RecursionGuard::depth_only(),
1697 ) {
1698 return extract_default_nix_package(path, resolved, scopes, depth);
1699 }
1700
1701 Err("default.nix did not contain a supported mkDerivation call".to_string())
1702 }
1703 Expr::Symbol(_) => extract_flake_compat_package_from_expr(path, expr, scopes, depth)
1704 .ok_or_else(|| "default.nix did not contain a supported mkDerivation call".to_string()),
1705 _ => Err("default.nix did not contain a supported mkDerivation call".to_string()),
1706 }
1707}
1708
1709fn build_default_package_from_attrset(
1710 path: &Path,
1711 derivation: &[(Vec<String>, Expr)],
1712 scopes: &[&[(Vec<String>, Expr)]],
1713) -> Result<PackageData, String> {
1714 let mut package = default_default_nix_package_data();
1715 package.name = find_string_attr_with_scopes(derivation, &["pname"], scopes).or_else(|| {
1716 find_string_attr_with_scopes(derivation, &["name"], scopes)
1717 .map(|name| split_derivation_name(&name).0)
1718 });
1719 package.version =
1720 find_string_attr_with_scopes(derivation, &["version"], scopes).or_else(|| {
1721 find_string_attr_with_scopes(derivation, &["name"], scopes)
1722 .and_then(|name| split_derivation_name(&name).1)
1723 });
1724 package.description =
1725 find_string_attr_with_scopes(derivation, &["meta", "description"], scopes)
1726 .or_else(|| find_string_attr_with_scopes(derivation, &["description"], scopes));
1727 package.homepage_url = find_string_attr_with_scopes(derivation, &["meta", "homepage"], scopes)
1728 .or_else(|| find_string_attr_with_scopes(derivation, &["homepage"], scopes));
1729 package.extracted_license_statement = find_attr(
1730 derivation,
1731 &["meta", "license"],
1732 &mut RecursionGuard::depth_only(),
1733 )
1734 .and_then(|expr| {
1735 expr_to_scalar_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1736 })
1737 .or_else(|| {
1738 find_attr(derivation, &["license"], &mut RecursionGuard::depth_only()).and_then(|expr| {
1739 expr_to_scalar_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1740 })
1741 });
1742 package.dependencies = [
1743 build_list_dependencies(derivation, "nativeBuildInputs", false, scopes),
1744 build_list_dependencies(derivation, "buildInputs", true, scopes),
1745 build_list_dependencies(derivation, "propagatedBuildInputs", true, scopes),
1746 build_list_dependencies(derivation, "checkInputs", false, scopes),
1747 ]
1748 .concat();
1749 if package.name.is_none() {
1750 package.name = fallback_name(path).map(truncate_field);
1751 }
1752 package.purl = package
1753 .name
1754 .as_deref()
1755 .and_then(|name| build_nix_purl(name, package.version.as_deref()));
1756
1757 Ok(package)
1758}
1759
1760fn try_follow_local_nix_application(
1761 path: &Path,
1762 parts: &[Expr],
1763 scopes: &[&[(Vec<String>, Expr)]],
1764) -> Option<(Expr, std::path::PathBuf)> {
1765 let head = parts.first().and_then(expr_as_symbol)?;
1766 let is_supported_wrapper = head == "import" || head.ends_with("callPackage");
1767 if !is_supported_wrapper {
1768 return None;
1769 }
1770
1771 let local_path = parts.get(1).and_then(|expr| {
1772 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1773 })?;
1774 if !is_local_nix_path(&local_path) {
1775 return None;
1776 }
1777
1778 let resolved_path = resolve_local_nix_path(path, &local_path)?;
1779 let content = read_file_to_string(&resolved_path, None).ok()?;
1780 let expr = parse_nix_expr(&content).ok()?;
1781 Some((expr, resolved_path))
1782}
1783
1784fn try_follow_selected_local_import(
1785 path: &Path,
1786 target: &Expr,
1787 select_path: &[String],
1788 scopes: &[&[(Vec<String>, Expr)]],
1789) -> Option<(Expr, std::path::PathBuf)> {
1790 let Expr::Application(parts) = target else {
1791 return None;
1792 };
1793
1794 let (imported_expr, imported_path) = try_follow_local_nix_application(path, parts, scopes)?;
1795 let selected = attrset_entries(&imported_expr).and_then(|entries| {
1796 find_attr(
1797 entries,
1798 &select_path.iter().map(String::as_str).collect::<Vec<_>>(),
1799 &mut RecursionGuard::depth_only(),
1800 )
1801 })?;
1802 Some((selected.clone(), imported_path))
1803}
1804
1805fn extract_flake_compat_package_from_expr(
1806 path: &Path,
1807 expr: &Expr,
1808 scopes: &[&[(Vec<String>, Expr)]],
1809 depth: usize,
1810) -> Option<PackageData> {
1811 if depth > 2 {
1812 return None;
1813 }
1814
1815 match expr {
1816 Expr::Select {
1817 target,
1818 path: select_path,
1819 } => extract_flake_compat_package_from_select(path, target, select_path, scopes, depth),
1820 Expr::Let { bindings, body } => {
1821 let scopes = extend_scopes(scopes, bindings);
1822 extract_flake_compat_package_from_expr(path, body, &scopes, depth)
1823 }
1824 Expr::Symbol(symbol) => {
1825 if let Some((head, rest)) = symbol.split_once('.') {
1826 let select_path = rest.split('.').map(ToOwned::to_owned).collect::<Vec<_>>();
1827 resolve_symbol(head, scopes, &mut RecursionGuard::depth_only())
1828 .and_then(|resolved| {
1829 extract_flake_compat_package_from_select(
1830 path,
1831 resolved,
1832 &select_path,
1833 scopes,
1834 depth,
1835 )
1836 })
1837 .or_else(|| {
1838 let target = Expr::Symbol(head.to_string());
1839 extract_flake_compat_package_from_select(
1840 path,
1841 &target,
1842 &select_path,
1843 scopes,
1844 depth,
1845 )
1846 })
1847 .or_else(|| {
1848 resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only()).and_then(
1849 |resolved| {
1850 extract_flake_compat_package_from_expr(
1851 path, resolved, scopes, depth,
1852 )
1853 },
1854 )
1855 })
1856 } else {
1857 resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only()).and_then(
1858 |resolved| {
1859 extract_flake_compat_package_from_expr(path, resolved, scopes, depth)
1860 },
1861 )
1862 }
1863 }
1864 _ => None,
1865 }
1866}
1867
1868fn extract_flake_compat_package_from_select(
1869 path: &Path,
1870 target: &Expr,
1871 select_path: &[String],
1872 scopes: &[&[(Vec<String>, Expr)]],
1873 depth: usize,
1874) -> Option<PackageData> {
1875 if depth > 2 || select_path.first().map(String::as_str) != Some("defaultNix") {
1876 return None;
1877 }
1878
1879 let source_root = resolve_flake_compat_source_root(path, target, scopes, 0)?;
1880 let mut package = default_default_nix_package_data();
1881 package.name = source_root
1882 .file_name()
1883 .and_then(|name| name.to_str())
1884 .map(ToOwned::to_owned)
1885 .map(truncate_field)
1886 .or_else(|| fallback_name(path));
1887 package.purl = package
1888 .name
1889 .as_deref()
1890 .and_then(|name| build_nix_purl(name, None));
1891 mark_flake_compat_wrapper(&mut package);
1892 Some(package)
1893}
1894
1895fn resolve_flake_compat_source_root(
1896 path: &Path,
1897 target: &Expr,
1898 scopes: &[&[(Vec<String>, Expr)]],
1899 depth: usize,
1900) -> Option<std::path::PathBuf> {
1901 if depth > 8 {
1902 return None;
1903 }
1904
1905 match target {
1906 Expr::Application(parts) => source_root_from_flake_compat_application(path, parts, scopes),
1907 Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only())
1908 .and_then(|resolved| {
1909 resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)
1910 }),
1911 Expr::Let { bindings, body } => {
1912 let scopes = extend_scopes(scopes, bindings);
1913 resolve_flake_compat_source_root(path, body, &scopes, depth + 1)
1914 }
1915 Expr::Select {
1916 target: inner_target,
1917 path: inner_path,
1918 } => resolve_select(
1919 inner_target,
1920 inner_path,
1921 scopes,
1922 &mut RecursionGuard::depth_only(),
1923 )
1924 .and_then(|resolved| resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)),
1925 _ => None,
1926 }
1927}
1928
1929fn source_root_from_flake_compat_application(
1930 path: &Path,
1931 parts: &[Expr],
1932 scopes: &[&[(Vec<String>, Expr)]],
1933) -> Option<std::path::PathBuf> {
1934 let head = parts.first().and_then(expr_as_symbol)?;
1935 if head != "import" {
1936 return None;
1937 }
1938
1939 let import_path = parts.get(1).and_then(|expr| {
1940 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1941 })?;
1942 if !is_local_nix_path(&import_path) {
1943 return None;
1944 }
1945
1946 let args = parts.iter().find_map(attrset_entries)?;
1947 let src_value =
1948 find_attr(args, &["src"], &mut RecursionGuard::depth_only()).and_then(|expr| {
1949 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1950 })?;
1951 if !is_local_path(&src_value) {
1952 return None;
1953 }
1954
1955 resolve_local_path(path, &src_value)
1956}
1957
1958fn is_local_path(value: &str) -> bool {
1959 value.starts_with("./") || value.starts_with("../")
1960}
1961
1962fn is_local_nix_path(value: &str) -> bool {
1963 is_local_path(value) && value.ends_with(".nix")
1964}
1965
1966fn resolve_local_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1967 let base = path.parent()?;
1968 let resolved = base.join(value);
1969 resolved.exists().then_some(resolved)
1970}
1971
1972fn resolve_local_nix_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1973 resolve_local_path(path, value).filter(|resolved| resolved.is_file())
1974}
1975
1976fn extract_flake_compat_default_package_from_content(
1977 path: &Path,
1978 content: &str,
1979) -> Result<PackageData, String> {
1980 if !content.contains("defaultNix") || !content.contains("flake-compat.nix") {
1981 return Err("default.nix did not contain a supported mkDerivation call".to_string());
1982 }
1983
1984 let src_value = extract_local_flake_compat_src_value(content).unwrap_or("./.".to_string());
1985 let mut package = default_default_nix_package_data();
1986 package.name = normalize_local_source_root(path, &src_value)
1987 .and_then(|source_root| {
1988 source_root
1989 .file_name()
1990 .and_then(|name| name.to_str())
1991 .filter(|name| *name != ".")
1992 .map(ToOwned::to_owned)
1993 })
1994 .map(truncate_field)
1995 .or_else(|| fallback_name(path));
1996 if package.name.is_none() {
1997 return Err("default.nix did not contain a supported mkDerivation call".to_string());
1998 }
1999 package.purl = package
2000 .name
2001 .as_deref()
2002 .and_then(|name| build_nix_purl(name, None));
2003 mark_flake_compat_wrapper(&mut package);
2004 Ok(package)
2005}
2006
2007fn mark_flake_compat_wrapper(package: &mut PackageData) {
2008 let mut extra_data = package.extra_data.clone().unwrap_or_default();
2009 extra_data.insert(
2010 "nix_wrapper_kind".to_string(),
2011 JsonValue::String("flake_compat".to_string()),
2012 );
2013 package.extra_data = Some(extra_data);
2014}
2015
2016fn extract_local_flake_compat_src_value(content: &str) -> Option<String> {
2017 let src_index = content.find("src")?;
2018 let after_src = &content[src_index + 3..];
2019 let equals_index = after_src.find('=')?;
2020 let remainder = after_src[equals_index + 1..].trim_start();
2021 let end_index = remainder.find([';', '}', '\n']).unwrap_or(remainder.len());
2022 let candidate = remainder[..end_index].trim();
2023 if is_local_path(candidate) {
2024 Some(candidate.to_string())
2025 } else {
2026 None
2027 }
2028}
2029
2030fn normalize_local_source_root(path: &Path, value: &str) -> Option<std::path::PathBuf> {
2031 match value {
2032 "." | "./." => path.parent().map(|parent| parent.to_path_buf()),
2033 _ if value.ends_with("/.") => resolve_local_path(path, value.trim_end_matches("/.")),
2034 _ => resolve_local_path(path, value),
2035 }
2036}
2037
2038fn split_derivation_name(name: &str) -> (String, Option<String>) {
2039 let mut parts = name.rsplitn(2, '-');
2040 let maybe_version = parts
2041 .next()
2042 .filter(|value| value.chars().any(|ch| ch.is_ascii_digit()));
2043 let maybe_name = parts.next();
2044
2045 match (maybe_name, maybe_version) {
2046 (Some(package_name), Some(version)) => {
2047 (package_name.to_string(), Some(version.to_string()))
2048 }
2049 _ => (name.to_string(), None),
2050 }
2051}
2052
2053fn default_flake_package_data() -> PackageData {
2054 PackageData {
2055 package_type: Some(PackageType::Nix),
2056 primary_language: Some("Nix".to_string()),
2057 datasource_id: Some(DatasourceId::NixFlakeNix),
2058 ..Default::default()
2059 }
2060}
2061
2062fn default_flake_lock_package_data() -> PackageData {
2063 PackageData {
2064 package_type: Some(PackageType::Nix),
2065 primary_language: Some("JSON".to_string()),
2066 datasource_id: Some(DatasourceId::NixFlakeLock),
2067 ..Default::default()
2068 }
2069}
2070
2071fn default_default_nix_package_data() -> PackageData {
2072 PackageData {
2073 package_type: Some(PackageType::Nix),
2074 primary_language: Some("Nix".to_string()),
2075 datasource_id: Some(DatasourceId::NixDefaultNix),
2076 ..Default::default()
2077 }
2078}