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