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::{
10 MAX_ITERATION_COUNT, RecursionGuard, read_file_to_string, truncate_field,
11};
12
13use super::PackageParser;
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 guard: RecursionGuard<()>,
443}
444
445impl Parser {
446 fn new(tokens: Vec<Token>) -> Self {
447 Self {
448 tokens,
449 index: 0,
450 guard: RecursionGuard::depth_only(),
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.guard.descend() {
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 let result = self.parse_expr();
471 self.guard.ascend();
472 return result;
473 }
474
475 if self.looks_like_prefixed_lambda_binder_set()? {
476 self.index += 1;
477 self.skip_lambda_binder_set()?;
478 self.expect(&Token::Colon)?;
479 let result = self.parse_expr();
480 self.guard.ascend();
481 return result;
482 }
483
484 let first = self.parse_term()?;
485 if self.consume(&Token::Colon) {
486 let result = self.parse_expr();
487 self.guard.ascend();
488 return result;
489 }
490
491 let mut terms = vec![first];
492 while self.can_start_term() {
493 terms.push(self.parse_term()?);
494 }
495
496 let expr = if terms.len() == 1 {
497 terms
498 .into_iter()
499 .next()
500 .unwrap_or_else(|| Expr::Symbol(String::new()))
501 } else {
502 Expr::Application(terms)
503 };
504
505 let result = self.parse_postfix(expr);
506 self.guard.ascend();
507 result
508 }
509
510 fn parse_postfix(&mut self, mut expr: Expr) -> Result<Expr, String> {
511 while self.consume(&Token::Dot) {
512 let mut path = vec![self.take_attr_key()?];
513 while self.consume(&Token::Dot) {
514 path.push(self.take_attr_key()?);
515 }
516 expr = Expr::Select {
517 target: Box::new(expr),
518 path,
519 };
520 }
521
522 Ok(expr)
523 }
524
525 fn parse_term(&mut self) -> Result<Expr, String> {
526 match self.peek() {
527 Some(Token::Ident(keyword)) if keyword == "let" => self.parse_let_in_expr(),
528 Some(Token::Ident(keyword)) if keyword == "with" => {
529 self.index += 1;
530 let _ = self.parse_expr()?;
531 self.expect(&Token::Semicolon)?;
532 self.parse_expr()
533 }
534 Some(Token::Ident(keyword)) if keyword == "rec" => {
535 if matches!(self.peek_n(1), Some(Token::LBrace)) {
536 self.index += 1;
537 self.parse_attrset()
538 } else {
539 self.parse_symbol()
540 }
541 }
542 Some(Token::LBrace) => self.parse_attrset(),
543 Some(Token::LBracket) => self.parse_list(),
544 Some(Token::LParen) => {
545 self.index += 1;
546 let expr = self.parse_expr()?;
547 self.expect(&Token::RParen)?;
548 Ok(expr)
549 }
550 Some(Token::String(_)) => self.parse_string(),
551 Some(Token::Ident(_)) => self.parse_symbol(),
552 _ => Err("expected expression".to_string()),
553 }
554 }
555
556 fn parse_let_in_expr(&mut self) -> Result<Expr, String> {
557 self.take_exact_ident("let")?;
558 let mut bindings = Vec::new();
559
560 while !matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "in") {
561 if self.peek().is_none() {
562 return Err("unterminated let expression".to_string());
563 }
564
565 if bindings.len() >= MAX_ITERATION_COUNT {
566 warn!("parse_let_in_expr exceeded MAX_ITERATION_COUNT bindings limit");
567 break;
568 }
569
570 if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
571 bindings.extend(self.parse_inherit_entries()?);
572 continue;
573 }
574
575 let key = self.parse_attr_path()?;
576 self.expect(&Token::Equals)?;
577 let value = self.parse_expr()?;
578 self.expect(&Token::Semicolon)?;
579 bindings.push((key, value));
580 }
581
582 self.take_exact_ident("in")?;
583 let body = self.parse_expr()?;
584 Ok(Expr::Let {
585 bindings,
586 body: Box::new(body),
587 })
588 }
589
590 fn parse_attrset(&mut self) -> Result<Expr, String> {
591 self.expect(&Token::LBrace)?;
592 let mut entries = Vec::new();
593
594 loop {
595 if self.consume(&Token::RBrace) {
596 return Ok(Expr::AttrSet(entries));
597 }
598
599 if self.peek().is_none() {
600 return Err("unterminated attribute set".to_string());
601 }
602
603 if entries.len() >= MAX_ITERATION_COUNT {
604 warn!("parse_attrset exceeded MAX_ITERATION_COUNT entries limit");
605 break;
606 }
607
608 if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
609 entries.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 entries.push((key, value));
618 }
619
620 Ok(Expr::AttrSet(entries))
621 }
622
623 fn parse_attr_path(&mut self) -> Result<Vec<String>, String> {
624 let mut path = vec![self.take_attr_key()?];
625 while self.consume(&Token::Dot) {
626 path.push(self.take_attr_key()?);
627 }
628 Ok(path)
629 }
630
631 fn parse_inherit_entries(&mut self) -> Result<Vec<(Vec<String>, Expr)>, String> {
632 self.take_exact_ident("inherit")?;
633
634 let inherit_from = if self.consume(&Token::LParen) {
635 let expr = self.parse_expr()?;
636 self.expect(&Token::RParen)?;
637 Some(expr)
638 } else {
639 None
640 };
641
642 let mut entries = Vec::new();
643 while !self.consume(&Token::Semicolon) {
644 if self.peek().is_none() {
645 return Err("unterminated inherit statement".to_string());
646 }
647
648 if entries.len() >= MAX_ITERATION_COUNT {
649 warn!("parse_inherit_entries exceeded MAX_ITERATION_COUNT entries limit");
650 break;
651 }
652
653 let name = self.take_attr_key()?;
654 let value = match &inherit_from {
655 Some(source) => Expr::Select {
656 target: Box::new(source.clone()),
657 path: vec![name.clone()],
658 },
659 None => Expr::Symbol(name.clone()),
660 };
661 entries.push((vec![name], value));
662 }
663
664 Ok(entries)
665 }
666
667 fn parse_list(&mut self) -> Result<Expr, String> {
668 self.expect(&Token::LBracket)?;
669 let mut items = Vec::new();
670 while !self.consume(&Token::RBracket) {
671 if self.peek().is_none() {
672 return Err("unterminated list".to_string());
673 }
674
675 if items.len() >= MAX_ITERATION_COUNT {
676 warn!("parse_list exceeded MAX_ITERATION_COUNT items limit");
677 break;
678 }
679
680 items.push(self.parse_expr()?);
681 }
682 Ok(Expr::List(items))
683 }
684
685 fn parse_string(&mut self) -> Result<Expr, String> {
686 match self.next() {
687 Some(Token::String(value)) => Ok(Expr::String(value)),
688 _ => Err("expected string".to_string()),
689 }
690 }
691
692 fn parse_symbol(&mut self) -> Result<Expr, String> {
693 let mut parts = vec![self.take_ident()?];
694 while self.consume(&Token::Dot) {
695 parts.push(self.take_ident()?);
696 }
697 Ok(Expr::Symbol(parts.join(".")))
698 }
699
700 fn take_ident(&mut self) -> Result<String, String> {
701 match self.next() {
702 Some(Token::Ident(value)) => Ok(value),
703 _ => Err("expected identifier".to_string()),
704 }
705 }
706
707 fn take_exact_ident(&mut self, expected: &str) -> Result<(), String> {
708 match self.next() {
709 Some(Token::Ident(value)) if value == expected => Ok(()),
710 _ => Err(format!("expected {expected}")),
711 }
712 }
713
714 fn take_attr_key(&mut self) -> Result<String, String> {
715 match self.next() {
716 Some(Token::Ident(value)) | Some(Token::String(value)) => Ok(value),
717 _ => Err("expected attribute key".to_string()),
718 }
719 }
720
721 fn can_start_term(&self) -> bool {
722 matches!(
723 self.peek(),
724 Some(Token::LBrace)
725 | Some(Token::LBracket)
726 | Some(Token::LParen)
727 | Some(Token::String(_))
728 | Some(Token::Ident(_))
729 )
730 }
731
732 fn looks_like_lambda_binder_set(&self) -> Result<bool, String> {
733 if self.peek() != Some(&Token::LBrace) {
734 return Ok(false);
735 }
736
737 self.looks_like_lambda_binder_set_from(self.index)
738 }
739
740 fn looks_like_prefixed_lambda_binder_set(&self) -> Result<bool, String> {
741 match (self.peek(), self.peek_n(1)) {
742 (Some(Token::Ident(prefix)), Some(Token::LBrace)) if prefix.ends_with('@') => {
743 self.looks_like_lambda_binder_set_from(self.index + 1)
744 }
745 _ => Ok(false),
746 }
747 }
748
749 fn looks_like_lambda_binder_set_from(&self, start_index: usize) -> Result<bool, String> {
750 if self.tokens.get(start_index) != Some(&Token::LBrace) {
751 return Ok(false);
752 }
753
754 let mut depth = 0usize;
755 let mut index = start_index;
756
757 while let Some(token) = self.tokens.get(index) {
758 match token {
759 Token::LBrace => depth += 1,
760 Token::RBrace => {
761 depth = depth.saturating_sub(1);
762 if depth == 0 {
763 return Ok(matches!(self.tokens.get(index + 1), Some(Token::Colon)));
764 }
765 }
766 Token::Equals | Token::Semicolon if depth == 1 => return Ok(false),
767 _ => {}
768 }
769
770 index += 1;
771 }
772
773 Err("unterminated lambda binder set".to_string())
774 }
775
776 fn skip_lambda_binder_set(&mut self) -> Result<(), String> {
777 self.expect(&Token::LBrace)?;
778 let mut depth = 1usize;
779
780 while depth > 0 {
781 match self.next() {
782 Some(Token::LBrace) => depth += 1,
783 Some(Token::RBrace) => depth = depth.saturating_sub(1),
784 Some(_) => {}
785 None => return Err("unterminated lambda binder set".to_string()),
786 }
787 }
788
789 Ok(())
790 }
791
792 fn expect(&mut self, expected: &Token) -> Result<(), String> {
793 if self.consume(expected) {
794 Ok(())
795 } else {
796 Err(format!("expected {:?}", expected))
797 }
798 }
799
800 fn consume(&mut self, expected: &Token) -> bool {
801 if self.peek() == Some(expected) {
802 self.index += 1;
803 true
804 } else {
805 false
806 }
807 }
808
809 fn peek(&self) -> Option<&Token> {
810 self.tokens.get(self.index)
811 }
812
813 fn peek_n(&self, offset: usize) -> Option<&Token> {
814 self.tokens.get(self.index + offset)
815 }
816
817 fn next(&mut self) -> Option<Token> {
818 let token = self.tokens.get(self.index).cloned();
819 if token.is_some() {
820 self.index += 1;
821 }
822 token
823 }
824}
825
826fn parse_flake_nix(path: &Path, content: &str) -> Result<PackageData, String> {
827 let expr = parse_nix_expr(content)?;
828 let scopes = Vec::new();
829 let (root, scopes) =
830 root_attrset_with_scopes(&expr, &scopes, &mut RecursionGuard::depth_only())
831 .ok_or_else(|| "flake.nix root was not an attribute set".to_string())?;
832
833 let mut package = default_flake_package_data();
834 package.name = fallback_name(path).map(truncate_field);
835 package.description =
836 find_string_attr_with_scopes(root, &["description"], &scopes).map(truncate_field);
837 package.purl = package
838 .name
839 .as_deref()
840 .and_then(|name| build_nix_purl(name, None));
841 package.dependencies = build_flake_dependencies(root, &scopes);
842
843 Ok(package)
844}
845
846fn parse_default_nix(path: &Path, content: &str) -> Result<PackageData, String> {
847 match parse_nix_expr(content) {
848 Ok(expr) => extract_default_nix_package(path, &expr, &Vec::new(), 0)
849 .or_else(|_| extract_flake_compat_default_package_from_content(path, content)),
850 Err(parse_error) => extract_flake_compat_default_package_from_content(path, content)
851 .map_err(|_| parse_error),
852 }
853}
854
855fn parse_flake_lock(path: &Path, json: &JsonValue) -> Result<PackageData, String> {
856 let version = json
857 .get("version")
858 .and_then(JsonValue::as_i64)
859 .ok_or_else(|| "flake.lock missing integer version".to_string())?;
860 let root = json
861 .get("root")
862 .and_then(JsonValue::as_str)
863 .ok_or_else(|| "flake.lock missing root".to_string())?;
864 let nodes = json
865 .get("nodes")
866 .and_then(JsonValue::as_object)
867 .ok_or_else(|| "flake.lock missing nodes".to_string())?;
868 let root_node = nodes
869 .get(root)
870 .and_then(JsonValue::as_object)
871 .ok_or_else(|| "flake.lock root node missing".to_string())?;
872 let root_inputs = root_node
873 .get("inputs")
874 .and_then(JsonValue::as_object)
875 .ok_or_else(|| "flake.lock root node missing inputs".to_string())?;
876
877 let mut package = default_flake_lock_package_data();
878 package.name = fallback_name(path).map(truncate_field);
879 package.purl = package
880 .name
881 .as_deref()
882 .and_then(|name| build_nix_purl(name, None));
883
884 let mut extra_data = HashMap::new();
885 extra_data.insert("lock_version".to_string(), JsonValue::from(version));
886 extra_data.insert("root".to_string(), JsonValue::String(root.to_string()));
887 package.extra_data = Some(extra_data);
888
889 package.dependencies = root_inputs
890 .iter()
891 .take(MAX_ITERATION_COUNT)
892 .filter_map(|(input_name, node_ref)| build_lock_dependency(input_name, node_ref, nodes))
893 .collect();
894 package
895 .dependencies
896 .sort_by(|left, right| left.purl.cmp(&right.purl));
897
898 Ok(package)
899}
900
901fn build_lock_dependency(
902 input_name: &str,
903 node_ref: &JsonValue,
904 nodes: &serde_json::Map<String, JsonValue>,
905) -> Option<Dependency> {
906 let node_id = node_ref.as_str()?;
907 let node = nodes.get(node_id)?.as_object()?;
908 let locked = node.get("locked").and_then(JsonValue::as_object)?;
909 let revision = locked.get("rev").and_then(JsonValue::as_str);
910
911 let mut extra_data = HashMap::new();
912 for key in [
913 "type",
914 "owner",
915 "repo",
916 "narHash",
917 "lastModified",
918 "revCount",
919 "url",
920 "path",
921 "dir",
922 "host",
923 ] {
924 if let Some(value) = locked.get(key) {
925 extra_data.insert(normalize_extra_key(key), value.clone());
926 }
927 }
928 if let Some(value) = node.get("flake").and_then(JsonValue::as_bool) {
929 extra_data.insert("flake".to_string(), JsonValue::Bool(value));
930 }
931 if let Some(original) = node.get("original").and_then(JsonValue::as_object) {
932 if let Some(value) = original.get("type") {
933 extra_data.insert("original_type".to_string(), value.clone());
934 }
935 if let Some(value) = original.get("id") {
936 extra_data.insert("original_id".to_string(), value.clone());
937 }
938 if let Some(value) = original.get("ref") {
939 extra_data.insert("original_ref".to_string(), value.clone());
940 }
941 }
942
943 Some(Dependency {
944 purl: build_nix_purl(input_name, revision),
945 extracted_requirement: build_locked_requirement(locked, node.get("original"))
946 .map(truncate_field),
947 scope: Some("inputs".to_string()),
948 is_runtime: Some(false),
949 is_optional: Some(false),
950 is_pinned: Some(revision.is_some()),
951 is_direct: Some(true),
952 resolved_package: None,
953 extra_data: (!extra_data.is_empty()).then_some(extra_data),
954 })
955}
956
957fn build_locked_requirement(
958 locked: &serde_json::Map<String, JsonValue>,
959 original: Option<&JsonValue>,
960) -> Option<String> {
961 let source_type = locked.get("type").and_then(JsonValue::as_str).or_else(|| {
962 original
963 .and_then(|value| value.get("type"))
964 .and_then(JsonValue::as_str)
965 });
966
967 match source_type {
968 Some("github") => {
969 let owner = locked.get("owner").and_then(JsonValue::as_str)?;
970 let repo = locked.get("repo").and_then(JsonValue::as_str)?;
971 Some(format!("github:{owner}/{repo}"))
972 }
973 Some("indirect") => original
974 .and_then(|value| value.get("id"))
975 .and_then(JsonValue::as_str)
976 .map(ToOwned::to_owned),
977 _ => locked
978 .get("url")
979 .and_then(JsonValue::as_str)
980 .map(ToOwned::to_owned),
981 }
982}
983
984fn normalize_extra_key(key: &str) -> String {
985 match key {
986 "type" => "source_type".to_string(),
987 "narHash" => "nar_hash".to_string(),
988 "lastModified" => "last_modified".to_string(),
989 "revCount" => "rev_count".to_string(),
990 other => other.to_string(),
991 }
992}
993
994fn build_flake_dependencies(
995 root: &[(Vec<String>, Expr)],
996 scopes: &[&[(Vec<String>, Expr)]],
997) -> Vec<Dependency> {
998 let mut inputs: HashMap<String, FlakeInputInfo> = HashMap::new();
999
1000 for (path, expr) in root {
1001 if path.first().map(String::as_str) != Some("inputs") {
1002 continue;
1003 }
1004
1005 if path.len() == 1 {
1006 if let Some(entries) = attrset_entries(expr) {
1007 collect_input_entries(entries, scopes, &mut inputs, None);
1008 }
1009 continue;
1010 }
1011
1012 collect_input_path(&path[1..], expr, scopes, &mut inputs);
1013 }
1014
1015 let mut dependencies = inputs
1016 .into_iter()
1017 .map(|(name, info)| {
1018 let mut extra_data = HashMap::new();
1019 if info.follows.len() == 1 {
1020 extra_data.insert(
1021 "follows".to_string(),
1022 JsonValue::String(info.follows[0].clone()),
1023 );
1024 } else if !info.follows.is_empty() {
1025 extra_data.insert(
1026 "follows".to_string(),
1027 JsonValue::Array(
1028 info.follows
1029 .iter()
1030 .cloned()
1031 .map(JsonValue::String)
1032 .collect(),
1033 ),
1034 );
1035 }
1036 if let Some(flake) = info.flake {
1037 extra_data.insert("flake".to_string(), JsonValue::Bool(flake));
1038 }
1039
1040 Dependency {
1041 purl: build_nix_purl(&name, None),
1042 extracted_requirement: info.requirement.map(truncate_field),
1043 scope: Some("inputs".to_string()),
1044 is_runtime: Some(false),
1045 is_optional: Some(false),
1046 is_pinned: Some(false),
1047 is_direct: Some(true),
1048 resolved_package: None,
1049 extra_data: (!extra_data.is_empty()).then_some(extra_data),
1050 }
1051 })
1052 .collect::<Vec<_>>();
1053
1054 dependencies.sort_by(|left, right| left.purl.cmp(&right.purl));
1055 dependencies
1056}
1057
1058fn collect_input_entries(
1059 entries: &[(Vec<String>, Expr)],
1060 scopes: &[&[(Vec<String>, Expr)]],
1061 inputs: &mut HashMap<String, FlakeInputInfo>,
1062 current_input: Option<&str>,
1063) {
1064 for (path, expr) in entries {
1065 if let Some(input_name) = current_input {
1066 apply_input_field(
1067 inputs.entry(input_name.to_string()).or_default(),
1068 path,
1069 expr,
1070 scopes,
1071 );
1072 continue;
1073 }
1074
1075 collect_input_path(path, expr, scopes, inputs);
1076 }
1077}
1078
1079fn collect_input_path(
1080 path: &[String],
1081 expr: &Expr,
1082 scopes: &[&[(Vec<String>, Expr)]],
1083 inputs: &mut HashMap<String, FlakeInputInfo>,
1084) {
1085 let Some(input_name) = path.first() else {
1086 return;
1087 };
1088
1089 if path.len() == 1 {
1090 match expr {
1091 Expr::AttrSet(entries) => {
1092 collect_input_entries(entries, scopes, inputs, Some(input_name))
1093 }
1094 Expr::String(value) => {
1095 inputs.entry(input_name.clone()).or_default().requirement = Some(value.clone())
1096 }
1097 Expr::Symbol(value) => {
1098 inputs.entry(input_name.clone()).or_default().requirement =
1099 expr_as_string_with_scopes(
1100 &Expr::Symbol(value.clone()),
1101 scopes,
1102 &mut RecursionGuard::depth_only(),
1103 )
1104 }
1105 _ => {}
1106 }
1107 return;
1108 }
1109
1110 apply_input_field(
1111 inputs.entry(input_name.clone()).or_default(),
1112 &path[1..],
1113 expr,
1114 scopes,
1115 );
1116}
1117
1118fn apply_input_field(
1119 info: &mut FlakeInputInfo,
1120 path: &[String],
1121 expr: &Expr,
1122 scopes: &[&[(Vec<String>, Expr)]],
1123) {
1124 if path == ["url"] {
1125 info.requirement =
1126 expr_as_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only());
1127 return;
1128 }
1129
1130 if path == ["flake"] {
1131 info.flake = expr_as_bool_with_scopes(expr, scopes, &mut RecursionGuard::depth_only());
1132 return;
1133 }
1134
1135 if path.len() == 3
1136 && path[0] == "inputs"
1137 && path[2] == "follows"
1138 && let Some(value) =
1139 expr_as_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1140 {
1141 info.follows.push(value);
1142 }
1143}
1144
1145fn build_list_dependencies(
1146 entries: &[(Vec<String>, Expr)],
1147 field_name: &str,
1148 runtime: bool,
1149 scopes: &[&[(Vec<String>, Expr)]],
1150) -> Vec<Dependency> {
1151 let Some(expr) = find_attr(entries, &[field_name], &mut RecursionGuard::depth_only()) else {
1152 return Vec::new();
1153 };
1154 let Some(items) = list_items_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1155 else {
1156 return Vec::new();
1157 };
1158
1159 items
1160 .iter()
1161 .take(MAX_ITERATION_COUNT)
1162 .flat_map(|expr| {
1163 expr_to_dependency_symbols_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1164 })
1165 .filter_map(|symbol| {
1166 let name = symbol.rsplit('.').next()?.to_string();
1167 Some(Dependency {
1168 purl: build_nix_purl(&name, None),
1169 extracted_requirement: None,
1170 scope: Some(field_name.to_string()),
1171 is_runtime: Some(runtime),
1172 is_optional: Some(false),
1173 is_pinned: Some(false),
1174 is_direct: Some(true),
1175 resolved_package: None,
1176 extra_data: None,
1177 })
1178 })
1179 .collect()
1180}
1181
1182fn expr_to_dependency_symbols_with_scopes(
1183 expr: &Expr,
1184 scopes: &[&[(Vec<String>, Expr)]],
1185 guard: &mut RecursionGuard<()>,
1186) -> Vec<String> {
1187 if guard.descend() {
1188 warn!("expr_to_dependency_symbols_with_scopes exceeded MAX_RECURSION_DEPTH");
1189 return Vec::new();
1190 }
1191
1192 let result = match expr {
1193 Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only())
1194 .map(|resolved| expr_to_dependency_symbols_with_scopes(resolved, scopes, guard))
1195 .unwrap_or_else(|| vec![symbol.clone()]),
1196 Expr::Application(parts) => parts
1197 .iter()
1198 .filter_map(|expr| {
1199 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1200 })
1201 .collect(),
1202 Expr::Let { bindings, body } => {
1203 let scopes = extend_scopes(scopes, bindings);
1204 expr_to_dependency_symbols_with_scopes(body, &scopes, guard)
1205 }
1206 Expr::Select { .. } => {
1207 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1208 .into_iter()
1209 .collect()
1210 }
1211 _ => Vec::new(),
1212 };
1213 guard.ascend();
1214 result
1215}
1216
1217fn fallback_name(path: &Path) -> Option<String> {
1218 path.parent()
1219 .and_then(|parent| parent.file_name())
1220 .and_then(|name| name.to_str())
1221 .map(ToOwned::to_owned)
1222}
1223
1224fn build_nix_purl(name: &str, version: Option<&str>) -> Option<String> {
1225 let mut purl = PackageUrl::new(PackageType::Nix.as_str(), name).ok()?;
1226 if let Some(version) = version {
1227 purl.with_version(version).ok()?;
1228 }
1229 Some(truncate_field(purl.to_string()))
1230}
1231
1232fn parse_nix_expr(content: &str) -> Result<Expr, String> {
1233 let tokens = Lexer::new(content).tokenize()?;
1234 Parser::new(tokens).parse()
1235}
1236
1237fn attrset_entries(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1238 match expr {
1239 Expr::AttrSet(entries) => Some(entries),
1240 _ => None,
1241 }
1242}
1243
1244fn list_items_with_scopes<'a>(
1245 expr: &'a Expr,
1246 scopes: &[&'a [(Vec<String>, Expr)]],
1247 guard: &mut RecursionGuard<()>,
1248) -> Option<&'a [Expr]> {
1249 if guard.descend() {
1250 warn!("list_items_with_scopes exceeded MAX_RECURSION_DEPTH");
1251 return None;
1252 }
1253
1254 let result = match expr {
1255 Expr::List(items) => Some(items.as_slice()),
1256 Expr::Let { bindings, body } => {
1257 let scopes = extend_scopes(scopes, bindings);
1258 list_items_with_scopes(body, &scopes, guard)
1259 }
1260 Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only())
1261 .and_then(|resolved| list_items_with_scopes(resolved, scopes, guard)),
1262 Expr::Select { target, path } => {
1263 resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1264 .and_then(|resolved| list_items_with_scopes(resolved, scopes, guard))
1265 }
1266 _ => None,
1267 };
1268 guard.ascend();
1269 result
1270}
1271
1272fn expr_as_symbol(expr: &Expr) -> Option<String> {
1273 match expr {
1274 Expr::Symbol(value) => Some(value.clone()),
1275 _ => None,
1276 }
1277}
1278
1279fn expr_as_symbol_with_scopes(
1280 expr: &Expr,
1281 scopes: &[&[(Vec<String>, Expr)]],
1282 guard: &mut RecursionGuard<()>,
1283) -> Option<String> {
1284 if guard.descend() {
1285 warn!("expr_as_symbol_with_scopes exceeded MAX_RECURSION_DEPTH");
1286 return None;
1287 }
1288
1289 let result = match expr {
1290 Expr::Symbol(value) => resolve_symbol(value, scopes, &mut RecursionGuard::depth_only())
1291 .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes, guard))
1292 .or_else(|| Some(value.clone())),
1293 Expr::Select { target, path } => {
1294 resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1295 .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes, guard))
1296 }
1297 Expr::Let { bindings, body } => {
1298 let scopes = extend_scopes(scopes, bindings);
1299 expr_as_symbol_with_scopes(body, &scopes, guard)
1300 }
1301 _ => expr_as_symbol(expr),
1302 };
1303 guard.ascend();
1304 result
1305}
1306
1307fn expr_as_bool(expr: &Expr) -> Option<bool> {
1308 match expr {
1309 Expr::Symbol(value) if value == "true" => Some(true),
1310 Expr::Symbol(value) if value == "false" => Some(false),
1311 _ => None,
1312 }
1313}
1314
1315fn expr_as_bool_with_scopes(
1316 expr: &Expr,
1317 scopes: &[&[(Vec<String>, Expr)]],
1318 guard: &mut RecursionGuard<()>,
1319) -> Option<bool> {
1320 if guard.descend() {
1321 warn!("expr_as_bool_with_scopes exceeded MAX_RECURSION_DEPTH");
1322 return None;
1323 }
1324
1325 let result = match expr {
1326 Expr::Let { bindings, body } => {
1327 let scopes = extend_scopes(scopes, bindings);
1328 expr_as_bool_with_scopes(body, &scopes, guard)
1329 }
1330 Expr::Symbol(value) => resolve_symbol(value, scopes, &mut RecursionGuard::depth_only())
1331 .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes, guard))
1332 .or_else(|| expr_as_bool(expr)),
1333 Expr::Select { target, path } => {
1334 resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1335 .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes, guard))
1336 }
1337 _ => expr_as_bool(expr),
1338 };
1339 guard.ascend();
1340 result
1341}
1342
1343fn expr_as_string_with_scopes(
1344 expr: &Expr,
1345 scopes: &[&[(Vec<String>, Expr)]],
1346 guard: &mut RecursionGuard<()>,
1347) -> Option<String> {
1348 if guard.descend() {
1349 warn!("expr_as_string_with_scopes exceeded MAX_RECURSION_DEPTH");
1350 return None;
1351 }
1352
1353 let result = match expr {
1354 Expr::String(value) => Some(interpolate_string(value, scopes)),
1355 Expr::Symbol(value) => resolve_symbol(value, scopes, &mut RecursionGuard::depth_only())
1356 .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes, guard))
1357 .or_else(|| Some(value.clone())),
1358 Expr::Application(parts) => parts
1359 .last()
1360 .and_then(|expr| expr_as_string_with_scopes(expr, scopes, guard)),
1361 Expr::Let { bindings, body } => {
1362 let scopes = extend_scopes(scopes, bindings);
1363 expr_as_string_with_scopes(body, &scopes, guard)
1364 }
1365 Expr::Select { target, path } => {
1366 resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1367 .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes, guard))
1368 }
1369 _ => None,
1370 };
1371 guard.ascend();
1372 result
1373}
1374
1375fn expr_to_scalar_string_with_scopes(
1376 expr: &Expr,
1377 scopes: &[&[(Vec<String>, Expr)]],
1378 guard: &mut RecursionGuard<()>,
1379) -> Option<String> {
1380 if guard.descend() {
1381 warn!("expr_to_scalar_string_with_scopes exceeded MAX_RECURSION_DEPTH");
1382 return None;
1383 }
1384
1385 let result = match expr {
1386 Expr::Application(parts) => parts
1387 .last()
1388 .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes, guard)),
1389 _ => expr_as_string_with_scopes(expr, scopes, guard),
1390 };
1391 guard.ascend();
1392 result
1393}
1394
1395fn find_attr<'a>(
1396 entries: &'a [(Vec<String>, Expr)],
1397 path: &[&str],
1398 guard: &mut RecursionGuard<()>,
1399) -> Option<&'a Expr> {
1400 if guard.descend() {
1401 warn!("find_attr exceeded MAX_RECURSION_DEPTH");
1402 return None;
1403 }
1404
1405 let result = entries.iter().find_map(|(key, value)| {
1406 if key.iter().map(String::as_str).eq(path.iter().copied()) {
1407 return Some(value);
1408 }
1409
1410 if key.len() < path.len()
1411 && key
1412 .iter()
1413 .map(String::as_str)
1414 .eq(path[..key.len()].iter().copied())
1415 && let Expr::AttrSet(child_entries) = value
1416 && let Some(found) = find_attr(child_entries, &path[key.len()..], guard)
1417 {
1418 return Some(found);
1419 }
1420
1421 None
1422 });
1423
1424 guard.ascend();
1425 result
1426}
1427
1428fn find_string_attr_with_scopes(
1429 entries: &[(Vec<String>, Expr)],
1430 path: &[&str],
1431 scopes: &[&[(Vec<String>, Expr)]],
1432) -> Option<String> {
1433 find_attr(entries, path, &mut RecursionGuard::depth_only())
1434 .and_then(|expr| {
1435 expr_to_scalar_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1436 })
1437 .map(truncate_field)
1438}
1439
1440fn find_mk_derivation_attrset(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1441 match expr {
1442 Expr::Application(parts) => {
1443 let is_derivation = parts
1444 .first()
1445 .and_then(expr_as_symbol)
1446 .is_some_and(|symbol| symbol.ends_with("mkDerivation"));
1447 if is_derivation {
1448 return parts.iter().rev().find_map(attrset_entries);
1449 }
1450 None
1451 }
1452 _ => None,
1453 }
1454}
1455
1456fn extend_scopes<'a>(
1457 scopes: &[NixAttrEntriesRef<'a>],
1458 bindings: NixAttrEntriesRef<'a>,
1459) -> NixScopeStack<'a> {
1460 let mut extended = scopes.to_vec();
1461 extended.push(bindings);
1462 extended
1463}
1464
1465fn root_attrset_with_scopes<'a>(
1466 expr: &'a Expr,
1467 scopes: &[NixAttrEntriesRef<'a>],
1468 guard: &mut RecursionGuard<()>,
1469) -> Option<(NixAttrEntriesRef<'a>, NixScopeStack<'a>)> {
1470 if guard.descend() {
1471 warn!("root_attrset_with_scopes exceeded MAX_RECURSION_DEPTH");
1472 return None;
1473 }
1474
1475 let result = match expr {
1476 Expr::AttrSet(entries) => Some((entries.as_slice(), scopes.to_vec())),
1477 Expr::Let { bindings, body } => {
1478 let scopes = extend_scopes(scopes, bindings);
1479 root_attrset_with_scopes(body, &scopes, guard)
1480 }
1481 _ => None,
1482 };
1483 guard.ascend();
1484 result
1485}
1486
1487fn lookup_binding<'a>(scopes: &[NixAttrEntriesRef<'a>], name: &str) -> Option<&'a Expr> {
1488 scopes
1489 .iter()
1490 .rev()
1491 .find_map(|bindings| find_attr(bindings, &[name], &mut RecursionGuard::depth_only()))
1492}
1493
1494fn resolve_symbol<'a>(
1495 symbol: &str,
1496 scopes: &[NixAttrEntriesRef<'a>],
1497 guard: &mut RecursionGuard<()>,
1498) -> Option<&'a Expr> {
1499 if guard.descend() {
1500 return None;
1501 }
1502
1503 let mut parts = symbol.split('.');
1504 let head = parts.next()?;
1505 let mut expr = lookup_binding(scopes, head)?;
1506 let rest = parts.collect::<Vec<_>>();
1507 if rest.is_empty() {
1508 guard.ascend();
1509 return Some(expr);
1510 }
1511
1512 for segment in rest {
1513 expr = resolve_select(expr, &[segment.to_string()], scopes, guard)?;
1514 }
1515
1516 guard.ascend();
1517 Some(expr)
1518}
1519
1520fn resolve_select<'a>(
1521 target: &'a Expr,
1522 path: &[String],
1523 scopes: &[NixAttrEntriesRef<'a>],
1524 guard: &mut RecursionGuard<()>,
1525) -> Option<&'a Expr> {
1526 if guard.descend() {
1527 return None;
1528 }
1529
1530 let result = match target {
1531 Expr::AttrSet(entries) => find_attr(
1532 entries,
1533 &path.iter().map(String::as_str).collect::<Vec<_>>(),
1534 guard,
1535 ),
1536 Expr::Let { bindings, body } => {
1537 let scopes = extend_scopes(scopes, bindings);
1538 resolve_select(body, path, &scopes, guard)
1539 }
1540 Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, guard)
1541 .and_then(|resolved| resolve_select(resolved, path, scopes, guard)),
1542 Expr::Select {
1543 target: inner_target,
1544 path: inner_path,
1545 } => resolve_select(inner_target, inner_path, scopes, guard)
1546 .and_then(|resolved| resolve_select(resolved, path, scopes, guard)),
1547 _ => None,
1548 };
1549 guard.ascend();
1550 result
1551}
1552
1553fn interpolate_string(value: &str, scopes: &[&[(Vec<String>, Expr)]]) -> String {
1554 let mut result = String::new();
1555 let mut index = 0usize;
1556
1557 while let Some(relative_start) = value[index..].find("${") {
1558 let start = index + relative_start;
1559 result.push_str(&value[index..start]);
1560
1561 let placeholder_start = start + 2;
1562 let Some(relative_end) = value[placeholder_start..].find('}') else {
1563 result.push_str(&value[start..]);
1564 return result;
1565 };
1566 let end = placeholder_start + relative_end;
1567 let placeholder = value[placeholder_start..end].trim();
1568 if !placeholder.is_empty()
1569 && placeholder
1570 .chars()
1571 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
1572 && let Some(resolved) =
1573 resolve_symbol(placeholder, scopes, &mut RecursionGuard::depth_only())
1574 && let Some(replacement) =
1575 expr_as_string_with_scopes(resolved, scopes, &mut RecursionGuard::depth_only())
1576 {
1577 result.push_str(&replacement);
1578 } else {
1579 result.push_str(&value[start..=end]);
1580 }
1581
1582 index = end + 1;
1583 }
1584
1585 result.push_str(&value[index..]);
1586 result
1587}
1588
1589fn extract_default_nix_package(
1590 path: &Path,
1591 expr: &Expr,
1592 scopes: &[&[(Vec<String>, Expr)]],
1593 depth: usize,
1594) -> Result<PackageData, String> {
1595 if depth > 2 {
1596 return Err("default.nix exceeded supported local import depth".to_string());
1597 }
1598
1599 match expr {
1600 Expr::Let { bindings, body } => {
1601 let scopes = extend_scopes(scopes, bindings);
1602 extract_default_nix_package(path, body, &scopes, depth)
1603 }
1604 Expr::Application(parts) => {
1605 if let Some(derivation) = find_mk_derivation_attrset(expr) {
1606 return build_default_package_from_attrset(path, derivation, scopes);
1607 }
1608
1609 if let Some((imported_expr, imported_path)) =
1610 try_follow_local_nix_application(path, parts, scopes)
1611 {
1612 return extract_default_nix_package(
1613 &imported_path,
1614 &imported_expr,
1615 &Vec::new(),
1616 depth + 1,
1617 );
1618 }
1619
1620 if let Some(package) = parts
1621 .first()
1622 .and_then(|part| extract_flake_compat_package_from_expr(path, part, scopes, depth))
1623 {
1624 return Ok(package);
1625 }
1626
1627 Err("default.nix did not contain a supported mkDerivation call".to_string())
1628 }
1629 Expr::Select {
1630 target,
1631 path: select_path,
1632 } => {
1633 if let Some(package) =
1634 extract_flake_compat_package_from_select(path, target, select_path, scopes, depth)
1635 {
1636 return Ok(package);
1637 }
1638
1639 if let Some((imported_expr, imported_path)) =
1640 try_follow_selected_local_import(path, target, select_path, scopes)
1641 {
1642 return extract_default_nix_package(
1643 &imported_path,
1644 &imported_expr,
1645 &Vec::new(),
1646 depth + 1,
1647 );
1648 }
1649
1650 if let Some(resolved) = resolve_select(
1651 target,
1652 select_path,
1653 scopes,
1654 &mut RecursionGuard::depth_only(),
1655 ) {
1656 return extract_default_nix_package(path, resolved, scopes, depth);
1657 }
1658
1659 Err("default.nix did not contain a supported mkDerivation call".to_string())
1660 }
1661 Expr::Symbol(_) => extract_flake_compat_package_from_expr(path, expr, scopes, depth)
1662 .ok_or_else(|| "default.nix did not contain a supported mkDerivation call".to_string()),
1663 _ => Err("default.nix did not contain a supported mkDerivation call".to_string()),
1664 }
1665}
1666
1667fn build_default_package_from_attrset(
1668 path: &Path,
1669 derivation: &[(Vec<String>, Expr)],
1670 scopes: &[&[(Vec<String>, Expr)]],
1671) -> Result<PackageData, String> {
1672 let mut package = default_default_nix_package_data();
1673 package.name = find_string_attr_with_scopes(derivation, &["pname"], scopes).or_else(|| {
1674 find_string_attr_with_scopes(derivation, &["name"], scopes)
1675 .map(|name| split_derivation_name(&name).0)
1676 });
1677 package.version =
1678 find_string_attr_with_scopes(derivation, &["version"], scopes).or_else(|| {
1679 find_string_attr_with_scopes(derivation, &["name"], scopes)
1680 .and_then(|name| split_derivation_name(&name).1)
1681 });
1682 package.description =
1683 find_string_attr_with_scopes(derivation, &["meta", "description"], scopes)
1684 .or_else(|| find_string_attr_with_scopes(derivation, &["description"], scopes));
1685 package.homepage_url = find_string_attr_with_scopes(derivation, &["meta", "homepage"], scopes)
1686 .or_else(|| find_string_attr_with_scopes(derivation, &["homepage"], scopes));
1687 package.extracted_license_statement = find_attr(
1688 derivation,
1689 &["meta", "license"],
1690 &mut RecursionGuard::depth_only(),
1691 )
1692 .and_then(|expr| {
1693 expr_to_scalar_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1694 })
1695 .or_else(|| {
1696 find_attr(derivation, &["license"], &mut RecursionGuard::depth_only()).and_then(|expr| {
1697 expr_to_scalar_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1698 })
1699 });
1700 package.dependencies = [
1701 build_list_dependencies(derivation, "nativeBuildInputs", false, scopes),
1702 build_list_dependencies(derivation, "buildInputs", true, scopes),
1703 build_list_dependencies(derivation, "propagatedBuildInputs", true, scopes),
1704 build_list_dependencies(derivation, "checkInputs", false, scopes),
1705 ]
1706 .concat();
1707 if package.name.is_none() {
1708 package.name = fallback_name(path).map(truncate_field);
1709 }
1710 package.purl = package
1711 .name
1712 .as_deref()
1713 .and_then(|name| build_nix_purl(name, package.version.as_deref()));
1714
1715 Ok(package)
1716}
1717
1718fn try_follow_local_nix_application(
1719 path: &Path,
1720 parts: &[Expr],
1721 scopes: &[&[(Vec<String>, Expr)]],
1722) -> Option<(Expr, std::path::PathBuf)> {
1723 let head = parts.first().and_then(expr_as_symbol)?;
1724 let is_supported_wrapper = head == "import" || head.ends_with("callPackage");
1725 if !is_supported_wrapper {
1726 return None;
1727 }
1728
1729 let local_path = parts.get(1).and_then(|expr| {
1730 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1731 })?;
1732 if !is_local_nix_path(&local_path) {
1733 return None;
1734 }
1735
1736 let resolved_path = resolve_local_nix_path(path, &local_path)?;
1737 let content = read_file_to_string(&resolved_path, None).ok()?;
1738 let expr = parse_nix_expr(&content).ok()?;
1739 Some((expr, resolved_path))
1740}
1741
1742fn try_follow_selected_local_import(
1743 path: &Path,
1744 target: &Expr,
1745 select_path: &[String],
1746 scopes: &[&[(Vec<String>, Expr)]],
1747) -> Option<(Expr, std::path::PathBuf)> {
1748 let Expr::Application(parts) = target else {
1749 return None;
1750 };
1751
1752 let (imported_expr, imported_path) = try_follow_local_nix_application(path, parts, scopes)?;
1753 let selected = attrset_entries(&imported_expr).and_then(|entries| {
1754 find_attr(
1755 entries,
1756 &select_path.iter().map(String::as_str).collect::<Vec<_>>(),
1757 &mut RecursionGuard::depth_only(),
1758 )
1759 })?;
1760 Some((selected.clone(), imported_path))
1761}
1762
1763fn extract_flake_compat_package_from_expr(
1764 path: &Path,
1765 expr: &Expr,
1766 scopes: &[&[(Vec<String>, Expr)]],
1767 depth: usize,
1768) -> Option<PackageData> {
1769 if depth > 2 {
1770 return None;
1771 }
1772
1773 match expr {
1774 Expr::Select {
1775 target,
1776 path: select_path,
1777 } => extract_flake_compat_package_from_select(path, target, select_path, scopes, depth),
1778 Expr::Let { bindings, body } => {
1779 let scopes = extend_scopes(scopes, bindings);
1780 extract_flake_compat_package_from_expr(path, body, &scopes, depth)
1781 }
1782 Expr::Symbol(symbol) => {
1783 if let Some((head, rest)) = symbol.split_once('.') {
1784 let select_path = rest.split('.').map(ToOwned::to_owned).collect::<Vec<_>>();
1785 resolve_symbol(head, scopes, &mut RecursionGuard::depth_only())
1786 .and_then(|resolved| {
1787 extract_flake_compat_package_from_select(
1788 path,
1789 resolved,
1790 &select_path,
1791 scopes,
1792 depth,
1793 )
1794 })
1795 .or_else(|| {
1796 let target = Expr::Symbol(head.to_string());
1797 extract_flake_compat_package_from_select(
1798 path,
1799 &target,
1800 &select_path,
1801 scopes,
1802 depth,
1803 )
1804 })
1805 .or_else(|| {
1806 resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only()).and_then(
1807 |resolved| {
1808 extract_flake_compat_package_from_expr(
1809 path, resolved, scopes, depth,
1810 )
1811 },
1812 )
1813 })
1814 } else {
1815 resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only()).and_then(
1816 |resolved| {
1817 extract_flake_compat_package_from_expr(path, resolved, scopes, depth)
1818 },
1819 )
1820 }
1821 }
1822 _ => None,
1823 }
1824}
1825
1826fn extract_flake_compat_package_from_select(
1827 path: &Path,
1828 target: &Expr,
1829 select_path: &[String],
1830 scopes: &[&[(Vec<String>, Expr)]],
1831 depth: usize,
1832) -> Option<PackageData> {
1833 if depth > 2 || select_path.first().map(String::as_str) != Some("defaultNix") {
1834 return None;
1835 }
1836
1837 let source_root = resolve_flake_compat_source_root(path, target, scopes, 0)?;
1838 let mut package = default_default_nix_package_data();
1839 package.name = source_root
1840 .file_name()
1841 .and_then(|name| name.to_str())
1842 .map(ToOwned::to_owned)
1843 .map(truncate_field)
1844 .or_else(|| fallback_name(path));
1845 package.purl = package
1846 .name
1847 .as_deref()
1848 .and_then(|name| build_nix_purl(name, None));
1849 mark_flake_compat_wrapper(&mut package);
1850 Some(package)
1851}
1852
1853fn resolve_flake_compat_source_root(
1854 path: &Path,
1855 target: &Expr,
1856 scopes: &[&[(Vec<String>, Expr)]],
1857 depth: usize,
1858) -> Option<std::path::PathBuf> {
1859 if depth > 8 {
1860 return None;
1861 }
1862
1863 match target {
1864 Expr::Application(parts) => source_root_from_flake_compat_application(path, parts, scopes),
1865 Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only())
1866 .and_then(|resolved| {
1867 resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)
1868 }),
1869 Expr::Let { bindings, body } => {
1870 let scopes = extend_scopes(scopes, bindings);
1871 resolve_flake_compat_source_root(path, body, &scopes, depth + 1)
1872 }
1873 Expr::Select {
1874 target: inner_target,
1875 path: inner_path,
1876 } => resolve_select(
1877 inner_target,
1878 inner_path,
1879 scopes,
1880 &mut RecursionGuard::depth_only(),
1881 )
1882 .and_then(|resolved| resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)),
1883 _ => None,
1884 }
1885}
1886
1887fn source_root_from_flake_compat_application(
1888 path: &Path,
1889 parts: &[Expr],
1890 scopes: &[&[(Vec<String>, Expr)]],
1891) -> Option<std::path::PathBuf> {
1892 let head = parts.first().and_then(expr_as_symbol)?;
1893 if head != "import" {
1894 return None;
1895 }
1896
1897 let import_path = parts.get(1).and_then(|expr| {
1898 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1899 })?;
1900 if !is_local_nix_path(&import_path) {
1901 return None;
1902 }
1903
1904 let args = parts.iter().find_map(attrset_entries)?;
1905 let src_value =
1906 find_attr(args, &["src"], &mut RecursionGuard::depth_only()).and_then(|expr| {
1907 expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1908 })?;
1909 if !is_local_path(&src_value) {
1910 return None;
1911 }
1912
1913 resolve_local_path(path, &src_value)
1914}
1915
1916fn is_local_path(value: &str) -> bool {
1917 value.starts_with("./") || value.starts_with("../")
1918}
1919
1920fn is_local_nix_path(value: &str) -> bool {
1921 is_local_path(value) && value.ends_with(".nix")
1922}
1923
1924fn resolve_local_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1925 let base = path.parent()?;
1926 let resolved = base.join(value);
1927 resolved.exists().then_some(resolved)
1928}
1929
1930fn resolve_local_nix_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1931 resolve_local_path(path, value).filter(|resolved| resolved.is_file())
1932}
1933
1934fn extract_flake_compat_default_package_from_content(
1935 path: &Path,
1936 content: &str,
1937) -> Result<PackageData, String> {
1938 if !content.contains("defaultNix") || !content.contains("flake-compat.nix") {
1939 return Err("default.nix did not contain a supported mkDerivation call".to_string());
1940 }
1941
1942 let src_value = extract_local_flake_compat_src_value(content).unwrap_or("./.".to_string());
1943 let mut package = default_default_nix_package_data();
1944 package.name = normalize_local_source_root(path, &src_value)
1945 .and_then(|source_root| {
1946 source_root
1947 .file_name()
1948 .and_then(|name| name.to_str())
1949 .filter(|name| *name != ".")
1950 .map(ToOwned::to_owned)
1951 })
1952 .map(truncate_field)
1953 .or_else(|| fallback_name(path));
1954 if package.name.is_none() {
1955 return Err("default.nix did not contain a supported mkDerivation call".to_string());
1956 }
1957 package.purl = package
1958 .name
1959 .as_deref()
1960 .and_then(|name| build_nix_purl(name, None));
1961 mark_flake_compat_wrapper(&mut package);
1962 Ok(package)
1963}
1964
1965fn mark_flake_compat_wrapper(package: &mut PackageData) {
1966 let mut extra_data = package.extra_data.clone().unwrap_or_default();
1967 extra_data.insert(
1968 "nix_wrapper_kind".to_string(),
1969 JsonValue::String("flake_compat".to_string()),
1970 );
1971 package.extra_data = Some(extra_data);
1972}
1973
1974fn extract_local_flake_compat_src_value(content: &str) -> Option<String> {
1975 let src_index = content.find("src")?;
1976 let after_src = &content[src_index + 3..];
1977 let equals_index = after_src.find('=')?;
1978 let remainder = after_src[equals_index + 1..].trim_start();
1979 let end_index = remainder.find([';', '}', '\n']).unwrap_or(remainder.len());
1980 let candidate = remainder[..end_index].trim();
1981 if is_local_path(candidate) {
1982 Some(candidate.to_string())
1983 } else {
1984 None
1985 }
1986}
1987
1988fn normalize_local_source_root(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1989 match value {
1990 "." | "./." => path.parent().map(|parent| parent.to_path_buf()),
1991 _ if value.ends_with("/.") => resolve_local_path(path, value.trim_end_matches("/.")),
1992 _ => resolve_local_path(path, value),
1993 }
1994}
1995
1996fn split_derivation_name(name: &str) -> (String, Option<String>) {
1997 let mut parts = name.rsplitn(2, '-');
1998 let maybe_version = parts
1999 .next()
2000 .filter(|value| value.chars().any(|ch| ch.is_ascii_digit()));
2001 let maybe_name = parts.next();
2002
2003 match (maybe_name, maybe_version) {
2004 (Some(package_name), Some(version)) => {
2005 (package_name.to_string(), Some(version.to_string()))
2006 }
2007 _ => (name.to_string(), None),
2008 }
2009}
2010
2011fn default_flake_package_data() -> PackageData {
2012 PackageData {
2013 package_type: Some(PackageType::Nix),
2014 primary_language: Some("Nix".to_string()),
2015 datasource_id: Some(DatasourceId::NixFlakeNix),
2016 ..Default::default()
2017 }
2018}
2019
2020fn default_flake_lock_package_data() -> PackageData {
2021 PackageData {
2022 package_type: Some(PackageType::Nix),
2023 primary_language: Some("JSON".to_string()),
2024 datasource_id: Some(DatasourceId::NixFlakeLock),
2025 ..Default::default()
2026 }
2027}
2028
2029fn default_default_nix_package_data() -> PackageData {
2030 PackageData {
2031 package_type: Some(PackageType::Nix),
2032 primary_language: Some("Nix".to_string()),
2033 datasource_id: Some(DatasourceId::NixDefaultNix),
2034 ..Default::default()
2035 }
2036}
2037
2038crate::register_parser!(
2039 "Nix flake manifest",
2040 &["**/flake.nix"],
2041 "nix",
2042 "Nix",
2043 Some("https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-flake.html"),
2044);
2045
2046crate::register_parser!(
2047 "Nix flake lockfile",
2048 &["**/flake.lock"],
2049 "nix",
2050 "JSON",
2051 Some("https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-flake.html"),
2052);
2053
2054crate::register_parser!(
2055 "Nix derivation manifest",
2056 &["**/default.nix"],
2057 "nix",
2058 "Nix",
2059 Some("https://nix.dev/manual/nix/stable/language/derivations.html"),
2060);