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