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