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