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(error) => {
70 warn!("Failed to parse flake.nix at {:?}: {}", path, error);
71 vec![default_flake_package_data()]
72 }
73 }
74 }
75}
76
77pub struct NixDefaultParser;
78
79impl PackageParser for NixDefaultParser {
80 const PACKAGE_TYPE: PackageType = PackageType::Nix;
81
82 fn is_match(path: &Path) -> bool {
83 path.file_name().is_some_and(|name| name == "default.nix")
84 }
85
86 fn extract_packages(path: &Path) -> Vec<PackageData> {
87 let content = match fs::read_to_string(path) {
88 Ok(content) => content,
89 Err(error) => {
90 warn!("Failed to read default.nix at {:?}: {}", path, error);
91 return vec![default_default_nix_package_data()];
92 }
93 };
94
95 match parse_default_nix(path, &content) {
96 Ok(package) => vec![package],
97 Err(error) => {
98 warn!("Failed to parse default.nix at {:?}: {}", path, error);
99 vec![default_default_nix_package_data()]
100 }
101 }
102 }
103}
104
105#[derive(Clone, Debug)]
106enum Expr {
107 AttrSet(Vec<(Vec<String>, Expr)>),
108 List(Vec<Expr>),
109 String(String),
110 Symbol(String),
111 Application(Vec<Expr>),
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
115enum Token {
116 LBrace,
117 RBrace,
118 LBracket,
119 RBracket,
120 LParen,
121 RParen,
122 Equals,
123 Semicolon,
124 Colon,
125 Dot,
126 Comma,
127 String(String),
128 Ident(String),
129}
130
131#[derive(Default)]
132struct FlakeInputInfo {
133 requirement: Option<String>,
134 follows: Vec<String>,
135 flake: Option<bool>,
136}
137
138struct Lexer {
139 chars: Vec<char>,
140 index: usize,
141}
142
143impl Lexer {
144 fn new(input: &str) -> Self {
145 Self {
146 chars: input.chars().collect(),
147 index: 0,
148 }
149 }
150
151 fn tokenize(mut self) -> Result<Vec<Token>, String> {
152 let mut tokens = Vec::new();
153
154 while let Some(ch) = self.peek() {
155 if ch.is_whitespace() {
156 self.index += 1;
157 continue;
158 }
159
160 if ch == '#' {
161 self.skip_line_comment();
162 continue;
163 }
164
165 if ch == '/' && self.peek_n(1) == Some('*') {
166 self.skip_block_comment()?;
167 continue;
168 }
169
170 match ch {
171 '$' if self.peek_n(1) == Some('{') => {
172 tokens.push(Token::Ident(self.read_interpolation_literal()?));
173 }
174 '.' if self.peek_n(1) == Some('/') => {
175 tokens.push(Token::Ident(self.read_path_literal()?));
176 }
177 '.' if self.peek_n(1) == Some('.') && self.peek_n(2) == Some('/') => {
178 tokens.push(Token::Ident(self.read_path_literal()?));
179 }
180 '{' => {
181 self.index += 1;
182 tokens.push(Token::LBrace);
183 }
184 '}' => {
185 self.index += 1;
186 tokens.push(Token::RBrace);
187 }
188 '[' => {
189 self.index += 1;
190 tokens.push(Token::LBracket);
191 }
192 ']' => {
193 self.index += 1;
194 tokens.push(Token::RBracket);
195 }
196 '(' => {
197 self.index += 1;
198 tokens.push(Token::LParen);
199 }
200 ')' => {
201 self.index += 1;
202 tokens.push(Token::RParen);
203 }
204 '=' => {
205 self.index += 1;
206 tokens.push(Token::Equals);
207 }
208 ';' => {
209 self.index += 1;
210 tokens.push(Token::Semicolon);
211 }
212 ':' => {
213 self.index += 1;
214 tokens.push(Token::Colon);
215 }
216 '.' => {
217 self.index += 1;
218 tokens.push(Token::Dot);
219 }
220 ',' => {
221 self.index += 1;
222 tokens.push(Token::Comma);
223 }
224 '"' => tokens.push(Token::String(self.read_double_quoted_string()?)),
225 '\'' if self.peek_n(1) == Some('\'') => {
226 tokens.push(Token::String(self.read_indented_string()?));
227 }
228 _ => tokens.push(Token::Ident(self.read_ident()?)),
229 }
230 }
231
232 Ok(tokens)
233 }
234
235 fn peek(&self) -> Option<char> {
236 self.chars.get(self.index).copied()
237 }
238
239 fn peek_n(&self, offset: usize) -> Option<char> {
240 self.chars.get(self.index + offset).copied()
241 }
242
243 fn skip_line_comment(&mut self) {
244 while let Some(ch) = self.peek() {
245 self.index += 1;
246 if ch == '\n' {
247 break;
248 }
249 }
250 }
251
252 fn skip_block_comment(&mut self) -> Result<(), String> {
253 self.index += 2;
254 while let Some(ch) = self.peek() {
255 if ch == '*' && self.peek_n(1) == Some('/') {
256 self.index += 2;
257 return Ok(());
258 }
259 self.index += 1;
260 }
261 Err("unterminated block comment".to_string())
262 }
263
264 fn read_double_quoted_string(&mut self) -> Result<String, String> {
265 self.index += 1;
266 let mut result = String::new();
267 let mut escaped = false;
268
269 while let Some(ch) = self.peek() {
270 self.index += 1;
271 if escaped {
272 result.push(match ch {
273 'n' => '\n',
274 'r' => '\r',
275 't' => '\t',
276 '"' => '"',
277 '\\' => '\\',
278 other => other,
279 });
280 escaped = false;
281 continue;
282 }
283
284 if ch == '\\' {
285 escaped = true;
286 continue;
287 }
288
289 if ch == '$' && self.peek() == Some('{') {
290 result.push(ch);
291 result.push('{');
292 self.index += 1;
293 let mut interpolation_depth = 1usize;
294
295 while let Some(inner) = self.peek() {
296 self.index += 1;
297 result.push(inner);
298
299 match inner {
300 '{' => interpolation_depth += 1,
301 '}' => {
302 interpolation_depth = interpolation_depth.saturating_sub(1);
303 if interpolation_depth == 0 {
304 break;
305 }
306 }
307 _ => {}
308 }
309 }
310
311 if interpolation_depth != 0 {
312 return Err("unterminated string interpolation".to_string());
313 }
314
315 continue;
316 }
317
318 if ch == '"' {
319 return Ok(result);
320 }
321
322 result.push(ch);
323 }
324
325 Err("unterminated string".to_string())
326 }
327
328 fn read_path_literal(&mut self) -> Result<String, String> {
329 let start = self.index;
330
331 while let Some(ch) = self.peek() {
332 if ch.is_whitespace()
333 || matches!(
334 ch,
335 '{' | '}' | '[' | ']' | '(' | ')' | '=' | ';' | ':' | ',' | '"'
336 )
337 || (ch == '\'' && self.peek_n(1) == Some('\''))
338 || ch == '#'
339 {
340 break;
341 }
342
343 if ch == '/' && self.peek_n(1) == Some('*') {
344 break;
345 }
346
347 self.index += 1;
348 }
349
350 if self.index == start {
351 return Err("unexpected token".to_string());
352 }
353
354 Ok(self.chars[start..self.index].iter().collect())
355 }
356
357 fn read_interpolation_literal(&mut self) -> Result<String, String> {
358 let start = self.index;
359 self.index += 2;
360 let mut depth = 1usize;
361
362 while let Some(ch) = self.peek() {
363 self.index += 1;
364
365 match ch {
366 '{' => depth += 1,
367 '}' => {
368 depth = depth.saturating_sub(1);
369 if depth == 0 {
370 return Ok(self.chars[start..self.index].iter().collect());
371 }
372 }
373 _ => {}
374 }
375 }
376
377 Err("unterminated interpolation literal".to_string())
378 }
379
380 fn read_indented_string(&mut self) -> Result<String, String> {
381 self.index += 2;
382 let mut result = String::new();
383
384 while let Some(ch) = self.peek() {
385 if ch == '\'' && self.peek_n(1) == Some('\'') {
386 self.index += 2;
387 return Ok(result);
388 }
389 result.push(ch);
390 self.index += 1;
391 }
392
393 Err("unterminated indented string".to_string())
394 }
395
396 fn read_ident(&mut self) -> Result<String, String> {
397 let start = self.index;
398
399 while let Some(ch) = self.peek() {
400 if ch.is_whitespace()
401 || matches!(
402 ch,
403 '{' | '}' | '[' | ']' | '(' | ')' | '=' | ';' | ':' | ',' | '.' | '"'
404 )
405 || (ch == '\'' && self.peek_n(1) == Some('\''))
406 || ch == '#'
407 {
408 break;
409 }
410
411 if ch == '/' && self.peek_n(1) == Some('*') {
412 break;
413 }
414
415 self.index += 1;
416 }
417
418 if self.index == start {
419 return Err("unexpected token".to_string());
420 }
421
422 Ok(self.chars[start..self.index].iter().collect())
423 }
424}
425
426struct Parser {
427 tokens: Vec<Token>,
428 index: usize,
429}
430
431impl Parser {
432 fn new(tokens: Vec<Token>) -> Self {
433 Self { tokens, index: 0 }
434 }
435
436 fn parse(mut self) -> Result<Expr, String> {
437 let expr = self.parse_expr()?;
438 if self.peek().is_some() {
439 return Err("unexpected trailing tokens".to_string());
440 }
441 Ok(expr)
442 }
443
444 fn parse_expr(&mut self) -> Result<Expr, String> {
445 if self.peek() == Some(&Token::LBrace) && self.looks_like_lambda_binder_set()? {
446 self.skip_lambda_binder_set()?;
447 self.expect(&Token::Colon)?;
448 return self.parse_expr();
449 }
450
451 let first = self.parse_term()?;
452 if self.consume(&Token::Colon) {
453 return self.parse_expr();
454 }
455
456 let mut terms = vec![first];
457 while self.can_start_term() {
458 terms.push(self.parse_term()?);
459 }
460
461 if terms.len() == 1 {
462 Ok(terms.pop().expect("single term"))
463 } else {
464 Ok(Expr::Application(terms))
465 }
466 }
467
468 fn parse_term(&mut self) -> Result<Expr, String> {
469 match self.peek() {
470 Some(Token::Ident(keyword)) if keyword == "let" => self.parse_let_in_expr(),
471 Some(Token::Ident(keyword)) if keyword == "with" => {
472 self.index += 1;
473 let _ = self.parse_expr()?;
474 self.expect(&Token::Semicolon)?;
475 self.parse_expr()
476 }
477 Some(Token::Ident(keyword)) if keyword == "rec" => {
478 if matches!(self.peek_n(1), Some(Token::LBrace)) {
479 self.index += 1;
480 self.parse_attrset()
481 } else {
482 self.parse_symbol()
483 }
484 }
485 Some(Token::LBrace) => self.parse_attrset(),
486 Some(Token::LBracket) => self.parse_list(),
487 Some(Token::LParen) => {
488 self.index += 1;
489 let expr = self.parse_expr()?;
490 self.expect(&Token::RParen)?;
491 Ok(expr)
492 }
493 Some(Token::String(_)) => self.parse_string(),
494 Some(Token::Ident(_)) => self.parse_symbol(),
495 _ => Err("expected expression".to_string()),
496 }
497 }
498
499 fn parse_let_in_expr(&mut self) -> Result<Expr, String> {
500 self.take_exact_ident("let")?;
501
502 while !matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "in") {
503 if self.peek().is_none() {
504 return Err("unterminated let expression".to_string());
505 }
506
507 if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
508 self.skip_until_semicolon()?;
509 continue;
510 }
511
512 let _key = self.parse_attr_path()?;
513 self.expect(&Token::Equals)?;
514 let _value = self.parse_expr()?;
515 self.expect(&Token::Semicolon)?;
516 }
517
518 self.take_exact_ident("in")?;
519 self.parse_expr()
520 }
521
522 fn parse_attrset(&mut self) -> Result<Expr, String> {
523 self.expect(&Token::LBrace)?;
524 let mut entries = Vec::new();
525
526 loop {
527 if self.consume(&Token::RBrace) {
528 return Ok(Expr::AttrSet(entries));
529 }
530
531 if self.peek().is_none() {
532 return Err("unterminated attribute set".to_string());
533 }
534
535 if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
536 self.skip_until_semicolon()?;
537 continue;
538 }
539
540 let key = self.parse_attr_path()?;
541 self.expect(&Token::Equals)?;
542 let value = self.parse_expr()?;
543 self.expect(&Token::Semicolon)?;
544 entries.push((key, value));
545 }
546 }
547
548 fn parse_attr_path(&mut self) -> Result<Vec<String>, String> {
549 let mut path = vec![self.take_attr_key()?];
550 while self.consume(&Token::Dot) {
551 path.push(self.take_attr_key()?);
552 }
553 Ok(path)
554 }
555
556 fn parse_list(&mut self) -> Result<Expr, String> {
557 self.expect(&Token::LBracket)?;
558 let mut items = Vec::new();
559 while !self.consume(&Token::RBracket) {
560 if self.peek().is_none() {
561 return Err("unterminated list".to_string());
562 }
563 items.push(self.parse_expr()?);
564 }
565 Ok(Expr::List(items))
566 }
567
568 fn parse_string(&mut self) -> Result<Expr, String> {
569 match self.next() {
570 Some(Token::String(value)) => Ok(Expr::String(value)),
571 _ => Err("expected string".to_string()),
572 }
573 }
574
575 fn parse_symbol(&mut self) -> Result<Expr, String> {
576 let mut parts = vec![self.take_ident()?];
577 while self.consume(&Token::Dot) {
578 parts.push(self.take_ident()?);
579 }
580 Ok(Expr::Symbol(parts.join(".")))
581 }
582
583 fn take_ident(&mut self) -> Result<String, String> {
584 match self.next() {
585 Some(Token::Ident(value)) => Ok(value),
586 _ => Err("expected identifier".to_string()),
587 }
588 }
589
590 fn take_exact_ident(&mut self, expected: &str) -> Result<(), String> {
591 match self.next() {
592 Some(Token::Ident(value)) if value == expected => Ok(()),
593 _ => Err(format!("expected {expected}")),
594 }
595 }
596
597 fn take_attr_key(&mut self) -> Result<String, String> {
598 match self.next() {
599 Some(Token::Ident(value)) | Some(Token::String(value)) => Ok(value),
600 _ => Err("expected attribute key".to_string()),
601 }
602 }
603
604 fn skip_until_semicolon(&mut self) -> Result<(), String> {
605 while !self.consume(&Token::Semicolon) {
606 if self.peek().is_none() {
607 return Err("unterminated statement".to_string());
608 }
609 self.index += 1;
610 }
611 Ok(())
612 }
613
614 fn can_start_term(&self) -> bool {
615 matches!(
616 self.peek(),
617 Some(Token::LBrace)
618 | Some(Token::LBracket)
619 | Some(Token::LParen)
620 | Some(Token::String(_))
621 | Some(Token::Ident(_))
622 )
623 }
624
625 fn looks_like_lambda_binder_set(&self) -> Result<bool, String> {
626 if self.peek() != Some(&Token::LBrace) {
627 return Ok(false);
628 }
629
630 let mut depth = 0usize;
631 let mut index = self.index;
632
633 while let Some(token) = self.tokens.get(index) {
634 match token {
635 Token::LBrace => depth += 1,
636 Token::RBrace => {
637 depth = depth.saturating_sub(1);
638 if depth == 0 {
639 return Ok(matches!(self.tokens.get(index + 1), Some(Token::Colon)));
640 }
641 }
642 Token::Equals | Token::Semicolon if depth == 1 => return Ok(false),
643 _ => {}
644 }
645
646 index += 1;
647 }
648
649 Err("unterminated lambda binder set".to_string())
650 }
651
652 fn skip_lambda_binder_set(&mut self) -> Result<(), String> {
653 self.expect(&Token::LBrace)?;
654 let mut depth = 1usize;
655
656 while depth > 0 {
657 match self.next() {
658 Some(Token::LBrace) => depth += 1,
659 Some(Token::RBrace) => depth = depth.saturating_sub(1),
660 Some(_) => {}
661 None => return Err("unterminated lambda binder set".to_string()),
662 }
663 }
664
665 Ok(())
666 }
667
668 fn expect(&mut self, expected: &Token) -> Result<(), String> {
669 if self.consume(expected) {
670 Ok(())
671 } else {
672 Err(format!("expected {:?}", expected))
673 }
674 }
675
676 fn consume(&mut self, expected: &Token) -> bool {
677 if self.peek() == Some(expected) {
678 self.index += 1;
679 true
680 } else {
681 false
682 }
683 }
684
685 fn peek(&self) -> Option<&Token> {
686 self.tokens.get(self.index)
687 }
688
689 fn peek_n(&self, offset: usize) -> Option<&Token> {
690 self.tokens.get(self.index + offset)
691 }
692
693 fn next(&mut self) -> Option<Token> {
694 let token = self.tokens.get(self.index).cloned();
695 if token.is_some() {
696 self.index += 1;
697 }
698 token
699 }
700}
701
702fn parse_flake_nix(path: &Path, content: &str) -> Result<PackageData, String> {
703 let expr = parse_nix_expr(content)?;
704 let root = attrset_entries(&expr)
705 .ok_or_else(|| "flake.nix root was not an attribute set".to_string())?;
706
707 let mut package = default_flake_package_data();
708 package.name = fallback_name(path);
709 package.description = find_string_attr(root, &["description"]);
710 package.purl = package
711 .name
712 .as_deref()
713 .and_then(|name| build_nix_purl(name, None));
714 package.dependencies = build_flake_dependencies(root);
715
716 Ok(package)
717}
718
719fn parse_default_nix(path: &Path, content: &str) -> Result<PackageData, String> {
720 let expr = parse_nix_expr(content)?;
721 let derivation = find_mk_derivation_attrset(&expr)
722 .ok_or_else(|| "default.nix did not contain a supported mkDerivation call".to_string())?;
723
724 let mut package = default_default_nix_package_data();
725 package.name = find_string_attr(derivation, &["pname"]).or_else(|| {
726 find_string_attr(derivation, &["name"]).map(|name| split_derivation_name(&name).0)
727 });
728 package.version = find_string_attr(derivation, &["version"]).or_else(|| {
729 find_string_attr(derivation, &["name"]).and_then(|name| split_derivation_name(&name).1)
730 });
731 package.description = find_string_attr(derivation, &["meta", "description"])
732 .or_else(|| find_string_attr(derivation, &["description"]));
733 package.homepage_url = find_string_attr(derivation, &["meta", "homepage"])
734 .or_else(|| find_string_attr(derivation, &["homepage"]));
735 package.extracted_license_statement = find_attr(derivation, &["meta", "license"])
736 .and_then(expr_to_scalar_string)
737 .or_else(|| find_attr(derivation, &["license"]).and_then(expr_to_scalar_string));
738 package.dependencies = [
739 build_list_dependencies(derivation, "nativeBuildInputs", false),
740 build_list_dependencies(derivation, "buildInputs", true),
741 build_list_dependencies(derivation, "propagatedBuildInputs", true),
742 build_list_dependencies(derivation, "checkInputs", false),
743 ]
744 .concat();
745 if package.name.is_none() {
746 package.name = fallback_name(path);
747 }
748 package.purl = package
749 .name
750 .as_deref()
751 .and_then(|name| build_nix_purl(name, package.version.as_deref()));
752
753 Ok(package)
754}
755
756fn parse_flake_lock(path: &Path, json: &JsonValue) -> Result<PackageData, String> {
757 let version = json
758 .get("version")
759 .and_then(JsonValue::as_i64)
760 .ok_or_else(|| "flake.lock missing integer version".to_string())?;
761 let root = json
762 .get("root")
763 .and_then(JsonValue::as_str)
764 .ok_or_else(|| "flake.lock missing root".to_string())?;
765 let nodes = json
766 .get("nodes")
767 .and_then(JsonValue::as_object)
768 .ok_or_else(|| "flake.lock missing nodes".to_string())?;
769 let root_node = nodes
770 .get(root)
771 .and_then(JsonValue::as_object)
772 .ok_or_else(|| "flake.lock root node missing".to_string())?;
773 let root_inputs = root_node
774 .get("inputs")
775 .and_then(JsonValue::as_object)
776 .ok_or_else(|| "flake.lock root node missing inputs".to_string())?;
777
778 let mut package = default_flake_lock_package_data();
779 package.name = fallback_name(path);
780 package.purl = package
781 .name
782 .as_deref()
783 .and_then(|name| build_nix_purl(name, None));
784
785 let mut extra_data = HashMap::new();
786 extra_data.insert("lock_version".to_string(), JsonValue::from(version));
787 extra_data.insert("root".to_string(), JsonValue::String(root.to_string()));
788 package.extra_data = Some(extra_data);
789
790 package.dependencies = root_inputs
791 .iter()
792 .filter_map(|(input_name, node_ref)| build_lock_dependency(input_name, node_ref, nodes))
793 .collect();
794 package
795 .dependencies
796 .sort_by(|left, right| left.purl.cmp(&right.purl));
797
798 Ok(package)
799}
800
801fn build_lock_dependency(
802 input_name: &str,
803 node_ref: &JsonValue,
804 nodes: &serde_json::Map<String, JsonValue>,
805) -> Option<Dependency> {
806 let node_id = node_ref.as_str()?;
807 let node = nodes.get(node_id)?.as_object()?;
808 let locked = node.get("locked").and_then(JsonValue::as_object)?;
809 let revision = locked.get("rev").and_then(JsonValue::as_str);
810
811 let mut extra_data = HashMap::new();
812 for key in [
813 "type",
814 "owner",
815 "repo",
816 "narHash",
817 "lastModified",
818 "revCount",
819 "url",
820 "path",
821 "dir",
822 "host",
823 ] {
824 if let Some(value) = locked.get(key) {
825 extra_data.insert(normalize_extra_key(key), value.clone());
826 }
827 }
828 if let Some(value) = node.get("flake").and_then(JsonValue::as_bool) {
829 extra_data.insert("flake".to_string(), JsonValue::Bool(value));
830 }
831 if let Some(original) = node.get("original").and_then(JsonValue::as_object) {
832 if let Some(value) = original.get("type") {
833 extra_data.insert("original_type".to_string(), value.clone());
834 }
835 if let Some(value) = original.get("id") {
836 extra_data.insert("original_id".to_string(), value.clone());
837 }
838 if let Some(value) = original.get("ref") {
839 extra_data.insert("original_ref".to_string(), value.clone());
840 }
841 }
842
843 Some(Dependency {
844 purl: build_nix_purl(input_name, revision),
845 extracted_requirement: build_locked_requirement(locked, node.get("original")),
846 scope: Some("inputs".to_string()),
847 is_runtime: Some(false),
848 is_optional: Some(false),
849 is_pinned: Some(revision.is_some()),
850 is_direct: Some(true),
851 resolved_package: None,
852 extra_data: (!extra_data.is_empty()).then_some(extra_data),
853 })
854}
855
856fn build_locked_requirement(
857 locked: &serde_json::Map<String, JsonValue>,
858 original: Option<&JsonValue>,
859) -> Option<String> {
860 let source_type = locked.get("type").and_then(JsonValue::as_str).or_else(|| {
861 original
862 .and_then(|value| value.get("type"))
863 .and_then(JsonValue::as_str)
864 });
865
866 match source_type {
867 Some("github") => {
868 let owner = locked.get("owner").and_then(JsonValue::as_str)?;
869 let repo = locked.get("repo").and_then(JsonValue::as_str)?;
870 Some(format!("github:{owner}/{repo}"))
871 }
872 Some("indirect") => original
873 .and_then(|value| value.get("id"))
874 .and_then(JsonValue::as_str)
875 .map(ToOwned::to_owned),
876 _ => locked
877 .get("url")
878 .and_then(JsonValue::as_str)
879 .map(ToOwned::to_owned),
880 }
881}
882
883fn normalize_extra_key(key: &str) -> String {
884 match key {
885 "type" => "source_type".to_string(),
886 "narHash" => "nar_hash".to_string(),
887 "lastModified" => "last_modified".to_string(),
888 "revCount" => "rev_count".to_string(),
889 other => other.to_string(),
890 }
891}
892
893fn build_flake_dependencies(root: &[(Vec<String>, Expr)]) -> Vec<Dependency> {
894 let mut inputs: HashMap<String, FlakeInputInfo> = HashMap::new();
895
896 for (path, expr) in root {
897 if path.first().map(String::as_str) != Some("inputs") {
898 continue;
899 }
900
901 if path.len() == 1 {
902 if let Some(entries) = attrset_entries(expr) {
903 collect_input_entries(entries, &mut inputs, None);
904 }
905 continue;
906 }
907
908 collect_input_path(&path[1..], expr, &mut inputs);
909 }
910
911 let mut dependencies = inputs
912 .into_iter()
913 .map(|(name, info)| {
914 let mut extra_data = HashMap::new();
915 if info.follows.len() == 1 {
916 extra_data.insert(
917 "follows".to_string(),
918 JsonValue::String(info.follows[0].clone()),
919 );
920 } else if !info.follows.is_empty() {
921 extra_data.insert(
922 "follows".to_string(),
923 JsonValue::Array(
924 info.follows
925 .iter()
926 .cloned()
927 .map(JsonValue::String)
928 .collect(),
929 ),
930 );
931 }
932 if let Some(flake) = info.flake {
933 extra_data.insert("flake".to_string(), JsonValue::Bool(flake));
934 }
935
936 Dependency {
937 purl: build_nix_purl(&name, None),
938 extracted_requirement: info.requirement,
939 scope: Some("inputs".to_string()),
940 is_runtime: Some(false),
941 is_optional: Some(false),
942 is_pinned: Some(false),
943 is_direct: Some(true),
944 resolved_package: None,
945 extra_data: (!extra_data.is_empty()).then_some(extra_data),
946 }
947 })
948 .collect::<Vec<_>>();
949
950 dependencies.sort_by(|left, right| left.purl.cmp(&right.purl));
951 dependencies
952}
953
954fn collect_input_entries(
955 entries: &[(Vec<String>, Expr)],
956 inputs: &mut HashMap<String, FlakeInputInfo>,
957 current_input: Option<&str>,
958) {
959 for (path, expr) in entries {
960 if let Some(input_name) = current_input {
961 apply_input_field(
962 inputs.entry(input_name.to_string()).or_default(),
963 path,
964 expr,
965 );
966 continue;
967 }
968
969 collect_input_path(path, expr, inputs);
970 }
971}
972
973fn collect_input_path(path: &[String], expr: &Expr, inputs: &mut HashMap<String, FlakeInputInfo>) {
974 let Some(input_name) = path.first() else {
975 return;
976 };
977
978 if path.len() == 1 {
979 match expr {
980 Expr::AttrSet(entries) => collect_input_entries(entries, inputs, Some(input_name)),
981 Expr::String(value) => {
982 inputs.entry(input_name.clone()).or_default().requirement = Some(value.clone())
983 }
984 _ => {}
985 }
986 return;
987 }
988
989 apply_input_field(
990 inputs.entry(input_name.clone()).or_default(),
991 &path[1..],
992 expr,
993 );
994}
995
996fn apply_input_field(info: &mut FlakeInputInfo, path: &[String], expr: &Expr) {
997 if path == ["url"] {
998 info.requirement = expr_as_string(expr);
999 return;
1000 }
1001
1002 if path == ["flake"] {
1003 info.flake = expr_as_bool(expr);
1004 return;
1005 }
1006
1007 if path.len() == 3
1008 && path[0] == "inputs"
1009 && path[2] == "follows"
1010 && let Some(value) = expr_as_string(expr)
1011 {
1012 info.follows.push(value);
1013 }
1014}
1015
1016fn build_list_dependencies(
1017 entries: &[(Vec<String>, Expr)],
1018 field_name: &str,
1019 runtime: bool,
1020) -> Vec<Dependency> {
1021 let Some(expr) = find_attr(entries, &[field_name]) else {
1022 return Vec::new();
1023 };
1024 let Some(items) = list_items(expr) else {
1025 return Vec::new();
1026 };
1027
1028 items
1029 .iter()
1030 .flat_map(expr_to_dependency_symbols)
1031 .filter_map(|symbol| {
1032 let name = symbol.rsplit('.').next()?.to_string();
1033 Some(Dependency {
1034 purl: build_nix_purl(&name, None),
1035 extracted_requirement: None,
1036 scope: Some(field_name.to_string()),
1037 is_runtime: Some(runtime),
1038 is_optional: Some(false),
1039 is_pinned: Some(false),
1040 is_direct: Some(true),
1041 resolved_package: None,
1042 extra_data: None,
1043 })
1044 })
1045 .collect()
1046}
1047
1048fn expr_to_dependency_symbols(expr: &Expr) -> Vec<String> {
1049 match expr {
1050 Expr::Symbol(symbol) => vec![symbol.clone()],
1051 Expr::Application(parts) => parts.iter().filter_map(expr_as_symbol).collect(),
1052 _ => Vec::new(),
1053 }
1054}
1055
1056fn fallback_name(path: &Path) -> Option<String> {
1057 path.parent()
1058 .and_then(|parent| parent.file_name())
1059 .and_then(|name| name.to_str())
1060 .map(ToOwned::to_owned)
1061}
1062
1063fn build_nix_purl(name: &str, version: Option<&str>) -> Option<String> {
1064 let mut purl = PackageUrl::new(PackageType::Nix.as_str(), name).ok()?;
1065 if let Some(version) = version {
1066 purl.with_version(version).ok()?;
1067 }
1068 Some(purl.to_string())
1069}
1070
1071fn parse_nix_expr(content: &str) -> Result<Expr, String> {
1072 let tokens = Lexer::new(content).tokenize()?;
1073 Parser::new(tokens).parse()
1074}
1075
1076fn attrset_entries(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1077 match expr {
1078 Expr::AttrSet(entries) => Some(entries),
1079 _ => None,
1080 }
1081}
1082
1083fn list_items(expr: &Expr) -> Option<&[Expr]> {
1084 match expr {
1085 Expr::List(items) => Some(items),
1086 _ => None,
1087 }
1088}
1089
1090fn expr_as_string(expr: &Expr) -> Option<String> {
1091 match expr {
1092 Expr::String(value) => Some(value.clone()),
1093 Expr::Symbol(value) => Some(value.clone()),
1094 _ => None,
1095 }
1096}
1097
1098fn expr_as_symbol(expr: &Expr) -> Option<String> {
1099 match expr {
1100 Expr::Symbol(value) => Some(value.clone()),
1101 _ => None,
1102 }
1103}
1104
1105fn expr_as_bool(expr: &Expr) -> Option<bool> {
1106 match expr {
1107 Expr::Symbol(value) if value == "true" => Some(true),
1108 Expr::Symbol(value) if value == "false" => Some(false),
1109 _ => None,
1110 }
1111}
1112
1113fn expr_to_scalar_string(expr: &Expr) -> Option<String> {
1114 match expr {
1115 Expr::String(value) | Expr::Symbol(value) => Some(value.clone()),
1116 Expr::Application(parts) => parts.last().and_then(expr_to_scalar_string),
1117 _ => None,
1118 }
1119}
1120
1121fn find_attr<'a>(entries: &'a [(Vec<String>, Expr)], path: &[&str]) -> Option<&'a Expr> {
1122 for (key, value) in entries {
1123 if key.iter().map(String::as_str).eq(path.iter().copied()) {
1124 return Some(value);
1125 }
1126
1127 if key.len() < path.len()
1128 && key
1129 .iter()
1130 .map(String::as_str)
1131 .eq(path[..key.len()].iter().copied())
1132 && let Expr::AttrSet(child_entries) = value
1133 && let Some(found) = find_attr(child_entries, &path[key.len()..])
1134 {
1135 return Some(found);
1136 }
1137 }
1138
1139 None
1140}
1141
1142fn find_string_attr(entries: &[(Vec<String>, Expr)], path: &[&str]) -> Option<String> {
1143 find_attr(entries, path).and_then(expr_to_scalar_string)
1144}
1145
1146fn find_mk_derivation_attrset(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1147 match expr {
1148 Expr::Application(parts) => {
1149 let is_derivation = parts
1150 .first()
1151 .and_then(expr_as_symbol)
1152 .is_some_and(|symbol| symbol.ends_with("mkDerivation"));
1153 if is_derivation {
1154 return parts.iter().rev().find_map(attrset_entries);
1155 }
1156 None
1157 }
1158 _ => None,
1159 }
1160}
1161
1162fn split_derivation_name(name: &str) -> (String, Option<String>) {
1163 let mut parts = name.rsplitn(2, '-');
1164 let maybe_version = parts
1165 .next()
1166 .filter(|value| value.chars().any(|ch| ch.is_ascii_digit()));
1167 let maybe_name = parts.next();
1168
1169 match (maybe_name, maybe_version) {
1170 (Some(package_name), Some(version)) => {
1171 (package_name.to_string(), Some(version.to_string()))
1172 }
1173 _ => (name.to_string(), None),
1174 }
1175}
1176
1177fn default_flake_package_data() -> PackageData {
1178 PackageData {
1179 package_type: Some(PackageType::Nix),
1180 primary_language: Some("Nix".to_string()),
1181 datasource_id: Some(DatasourceId::NixFlakeNix),
1182 ..Default::default()
1183 }
1184}
1185
1186fn default_flake_lock_package_data() -> PackageData {
1187 PackageData {
1188 package_type: Some(PackageType::Nix),
1189 primary_language: Some("JSON".to_string()),
1190 datasource_id: Some(DatasourceId::NixFlakeLock),
1191 ..Default::default()
1192 }
1193}
1194
1195fn default_default_nix_package_data() -> PackageData {
1196 PackageData {
1197 package_type: Some(PackageType::Nix),
1198 primary_language: Some("Nix".to_string()),
1199 datasource_id: Some(DatasourceId::NixDefaultNix),
1200 ..Default::default()
1201 }
1202}
1203
1204crate::register_parser!(
1205 "Nix flake manifest",
1206 &["**/flake.nix"],
1207 "nix",
1208 "Nix",
1209 Some("https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-flake.html"),
1210);
1211
1212crate::register_parser!(
1213 "Nix flake lockfile",
1214 &["**/flake.lock"],
1215 "nix",
1216 "JSON",
1217 Some("https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-flake.html"),
1218);
1219
1220crate::register_parser!(
1221 "Nix derivation manifest",
1222 &["**/default.nix"],
1223 "nix",
1224 "Nix",
1225 Some("https://nix.dev/manual/nix/stable/language/derivations.html"),
1226);