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