1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4
5use log::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 == "with" => {
381 self.index += 1;
382 let _ = self.parse_expr()?;
383 self.expect(&Token::Semicolon)?;
384 self.parse_expr()
385 }
386 Some(Token::Ident(keyword)) if keyword == "rec" => {
387 if matches!(self.peek_n(1), Some(Token::LBrace)) {
388 self.index += 1;
389 self.parse_attrset()
390 } else {
391 self.parse_symbol()
392 }
393 }
394 Some(Token::LBrace) => self.parse_attrset(),
395 Some(Token::LBracket) => self.parse_list(),
396 Some(Token::LParen) => {
397 self.index += 1;
398 let expr = self.parse_expr()?;
399 self.expect(&Token::RParen)?;
400 Ok(expr)
401 }
402 Some(Token::String(_)) => self.parse_string(),
403 Some(Token::Ident(_)) => self.parse_symbol(),
404 _ => Err("expected expression".to_string()),
405 }
406 }
407
408 fn parse_attrset(&mut self) -> Result<Expr, String> {
409 self.expect(&Token::LBrace)?;
410 let mut entries = Vec::new();
411
412 loop {
413 if self.consume(&Token::RBrace) {
414 return Ok(Expr::AttrSet(entries));
415 }
416
417 if self.peek().is_none() {
418 return Err("unterminated attribute set".to_string());
419 }
420
421 if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
422 self.skip_until_semicolon()?;
423 continue;
424 }
425
426 let key = self.parse_attr_path()?;
427 self.expect(&Token::Equals)?;
428 let value = self.parse_expr()?;
429 self.expect(&Token::Semicolon)?;
430 entries.push((key, value));
431 }
432 }
433
434 fn parse_attr_path(&mut self) -> Result<Vec<String>, String> {
435 let mut path = vec![self.take_ident()?];
436 while self.consume(&Token::Dot) {
437 path.push(self.take_ident()?);
438 }
439 Ok(path)
440 }
441
442 fn parse_list(&mut self) -> Result<Expr, String> {
443 self.expect(&Token::LBracket)?;
444 let mut items = Vec::new();
445 while !self.consume(&Token::RBracket) {
446 if self.peek().is_none() {
447 return Err("unterminated list".to_string());
448 }
449 items.push(self.parse_expr()?);
450 }
451 Ok(Expr::List(items))
452 }
453
454 fn parse_string(&mut self) -> Result<Expr, String> {
455 match self.next() {
456 Some(Token::String(value)) => Ok(Expr::String(value)),
457 _ => Err("expected string".to_string()),
458 }
459 }
460
461 fn parse_symbol(&mut self) -> Result<Expr, String> {
462 let mut parts = vec![self.take_ident()?];
463 while self.consume(&Token::Dot) {
464 parts.push(self.take_ident()?);
465 }
466 Ok(Expr::Symbol(parts.join(".")))
467 }
468
469 fn take_ident(&mut self) -> Result<String, String> {
470 match self.next() {
471 Some(Token::Ident(value)) => Ok(value),
472 _ => Err("expected identifier".to_string()),
473 }
474 }
475
476 fn skip_until_semicolon(&mut self) -> Result<(), String> {
477 while !self.consume(&Token::Semicolon) {
478 if self.peek().is_none() {
479 return Err("unterminated statement".to_string());
480 }
481 self.index += 1;
482 }
483 Ok(())
484 }
485
486 fn can_start_term(&self) -> bool {
487 matches!(
488 self.peek(),
489 Some(Token::LBrace)
490 | Some(Token::LBracket)
491 | Some(Token::LParen)
492 | Some(Token::String(_))
493 | Some(Token::Ident(_))
494 )
495 }
496
497 fn looks_like_lambda_binder_set(&self) -> Result<bool, String> {
498 if self.peek() != Some(&Token::LBrace) {
499 return Ok(false);
500 }
501
502 let mut depth = 0usize;
503 let mut index = self.index;
504
505 while let Some(token) = self.tokens.get(index) {
506 match token {
507 Token::LBrace => depth += 1,
508 Token::RBrace => {
509 depth = depth.saturating_sub(1);
510 if depth == 0 {
511 return Ok(matches!(self.tokens.get(index + 1), Some(Token::Colon)));
512 }
513 }
514 Token::Equals | Token::Semicolon if depth == 1 => return Ok(false),
515 _ => {}
516 }
517
518 index += 1;
519 }
520
521 Err("unterminated lambda binder set".to_string())
522 }
523
524 fn skip_lambda_binder_set(&mut self) -> Result<(), String> {
525 self.expect(&Token::LBrace)?;
526 let mut depth = 1usize;
527
528 while depth > 0 {
529 match self.next() {
530 Some(Token::LBrace) => depth += 1,
531 Some(Token::RBrace) => depth = depth.saturating_sub(1),
532 Some(_) => {}
533 None => return Err("unterminated lambda binder set".to_string()),
534 }
535 }
536
537 Ok(())
538 }
539
540 fn expect(&mut self, expected: &Token) -> Result<(), String> {
541 if self.consume(expected) {
542 Ok(())
543 } else {
544 Err(format!("expected {:?}", expected))
545 }
546 }
547
548 fn consume(&mut self, expected: &Token) -> bool {
549 if self.peek() == Some(expected) {
550 self.index += 1;
551 true
552 } else {
553 false
554 }
555 }
556
557 fn peek(&self) -> Option<&Token> {
558 self.tokens.get(self.index)
559 }
560
561 fn peek_n(&self, offset: usize) -> Option<&Token> {
562 self.tokens.get(self.index + offset)
563 }
564
565 fn next(&mut self) -> Option<Token> {
566 let token = self.tokens.get(self.index).cloned();
567 if token.is_some() {
568 self.index += 1;
569 }
570 token
571 }
572}
573
574fn parse_flake_nix(path: &Path, content: &str) -> Result<PackageData, String> {
575 let expr = parse_nix_expr(content)?;
576 let root = attrset_entries(&expr)
577 .ok_or_else(|| "flake.nix root was not an attribute set".to_string())?;
578
579 let mut package = default_flake_package_data();
580 package.name = fallback_name(path);
581 package.description = find_string_attr(root, &["description"]);
582 package.purl = package
583 .name
584 .as_deref()
585 .and_then(|name| build_nix_purl(name, None));
586 package.dependencies = build_flake_dependencies(root);
587
588 Ok(package)
589}
590
591fn parse_default_nix(path: &Path, content: &str) -> Result<PackageData, String> {
592 let expr = parse_nix_expr(content)?;
593 let derivation = find_mk_derivation_attrset(&expr)
594 .ok_or_else(|| "default.nix did not contain a supported mkDerivation call".to_string())?;
595
596 let mut package = default_default_nix_package_data();
597 package.name = find_string_attr(derivation, &["pname"]).or_else(|| {
598 find_string_attr(derivation, &["name"]).map(|name| split_derivation_name(&name).0)
599 });
600 package.version = find_string_attr(derivation, &["version"]).or_else(|| {
601 find_string_attr(derivation, &["name"]).and_then(|name| split_derivation_name(&name).1)
602 });
603 package.description = find_string_attr(derivation, &["meta", "description"])
604 .or_else(|| find_string_attr(derivation, &["description"]));
605 package.homepage_url = find_string_attr(derivation, &["meta", "homepage"])
606 .or_else(|| find_string_attr(derivation, &["homepage"]));
607 package.extracted_license_statement = find_attr(derivation, &["meta", "license"])
608 .and_then(expr_to_scalar_string)
609 .or_else(|| find_attr(derivation, &["license"]).and_then(expr_to_scalar_string));
610 package.dependencies = [
611 build_list_dependencies(derivation, "nativeBuildInputs", false),
612 build_list_dependencies(derivation, "buildInputs", true),
613 build_list_dependencies(derivation, "propagatedBuildInputs", true),
614 build_list_dependencies(derivation, "checkInputs", false),
615 ]
616 .concat();
617 if package.name.is_none() {
618 package.name = fallback_name(path);
619 }
620 package.purl = package
621 .name
622 .as_deref()
623 .and_then(|name| build_nix_purl(name, package.version.as_deref()));
624
625 Ok(package)
626}
627
628fn parse_flake_lock(path: &Path, json: &JsonValue) -> Result<PackageData, String> {
629 let version = json
630 .get("version")
631 .and_then(JsonValue::as_i64)
632 .ok_or_else(|| "flake.lock missing integer version".to_string())?;
633 let root = json
634 .get("root")
635 .and_then(JsonValue::as_str)
636 .ok_or_else(|| "flake.lock missing root".to_string())?;
637 let nodes = json
638 .get("nodes")
639 .and_then(JsonValue::as_object)
640 .ok_or_else(|| "flake.lock missing nodes".to_string())?;
641 let root_node = nodes
642 .get(root)
643 .and_then(JsonValue::as_object)
644 .ok_or_else(|| "flake.lock root node missing".to_string())?;
645 let root_inputs = root_node
646 .get("inputs")
647 .and_then(JsonValue::as_object)
648 .ok_or_else(|| "flake.lock root node missing inputs".to_string())?;
649
650 let mut package = default_flake_lock_package_data();
651 package.name = fallback_name(path);
652 package.purl = package
653 .name
654 .as_deref()
655 .and_then(|name| build_nix_purl(name, None));
656
657 let mut extra_data = HashMap::new();
658 extra_data.insert("lock_version".to_string(), JsonValue::from(version));
659 extra_data.insert("root".to_string(), JsonValue::String(root.to_string()));
660 package.extra_data = Some(extra_data);
661
662 package.dependencies = root_inputs
663 .iter()
664 .filter_map(|(input_name, node_ref)| build_lock_dependency(input_name, node_ref, nodes))
665 .collect();
666 package
667 .dependencies
668 .sort_by(|left, right| left.purl.cmp(&right.purl));
669
670 Ok(package)
671}
672
673fn build_lock_dependency(
674 input_name: &str,
675 node_ref: &JsonValue,
676 nodes: &serde_json::Map<String, JsonValue>,
677) -> Option<Dependency> {
678 let node_id = node_ref.as_str()?;
679 let node = nodes.get(node_id)?.as_object()?;
680 let locked = node.get("locked").and_then(JsonValue::as_object)?;
681 let revision = locked.get("rev").and_then(JsonValue::as_str);
682
683 let mut extra_data = HashMap::new();
684 for key in [
685 "type",
686 "owner",
687 "repo",
688 "narHash",
689 "lastModified",
690 "revCount",
691 "url",
692 "path",
693 "dir",
694 "host",
695 ] {
696 if let Some(value) = locked.get(key) {
697 extra_data.insert(normalize_extra_key(key), value.clone());
698 }
699 }
700 if let Some(value) = node.get("flake").and_then(JsonValue::as_bool) {
701 extra_data.insert("flake".to_string(), JsonValue::Bool(value));
702 }
703 if let Some(original) = node.get("original").and_then(JsonValue::as_object) {
704 if let Some(value) = original.get("type") {
705 extra_data.insert("original_type".to_string(), value.clone());
706 }
707 if let Some(value) = original.get("id") {
708 extra_data.insert("original_id".to_string(), value.clone());
709 }
710 if let Some(value) = original.get("ref") {
711 extra_data.insert("original_ref".to_string(), value.clone());
712 }
713 }
714
715 Some(Dependency {
716 purl: build_nix_purl(input_name, revision),
717 extracted_requirement: build_locked_requirement(locked, node.get("original")),
718 scope: Some("inputs".to_string()),
719 is_runtime: Some(false),
720 is_optional: Some(false),
721 is_pinned: Some(revision.is_some()),
722 is_direct: Some(true),
723 resolved_package: None,
724 extra_data: (!extra_data.is_empty()).then_some(extra_data),
725 })
726}
727
728fn build_locked_requirement(
729 locked: &serde_json::Map<String, JsonValue>,
730 original: Option<&JsonValue>,
731) -> Option<String> {
732 let source_type = locked.get("type").and_then(JsonValue::as_str).or_else(|| {
733 original
734 .and_then(|value| value.get("type"))
735 .and_then(JsonValue::as_str)
736 });
737
738 match source_type {
739 Some("github") => {
740 let owner = locked.get("owner").and_then(JsonValue::as_str)?;
741 let repo = locked.get("repo").and_then(JsonValue::as_str)?;
742 Some(format!("github:{owner}/{repo}"))
743 }
744 Some("indirect") => original
745 .and_then(|value| value.get("id"))
746 .and_then(JsonValue::as_str)
747 .map(ToOwned::to_owned),
748 _ => locked
749 .get("url")
750 .and_then(JsonValue::as_str)
751 .map(ToOwned::to_owned),
752 }
753}
754
755fn normalize_extra_key(key: &str) -> String {
756 match key {
757 "type" => "source_type".to_string(),
758 "narHash" => "nar_hash".to_string(),
759 "lastModified" => "last_modified".to_string(),
760 "revCount" => "rev_count".to_string(),
761 other => other.to_string(),
762 }
763}
764
765fn build_flake_dependencies(root: &[(Vec<String>, Expr)]) -> Vec<Dependency> {
766 let mut inputs: HashMap<String, FlakeInputInfo> = HashMap::new();
767
768 for (path, expr) in root {
769 if path.first().map(String::as_str) != Some("inputs") {
770 continue;
771 }
772
773 if path.len() == 1 {
774 if let Some(entries) = attrset_entries(expr) {
775 collect_input_entries(entries, &mut inputs, None);
776 }
777 continue;
778 }
779
780 collect_input_path(&path[1..], expr, &mut inputs);
781 }
782
783 let mut dependencies = inputs
784 .into_iter()
785 .map(|(name, info)| {
786 let mut extra_data = HashMap::new();
787 if info.follows.len() == 1 {
788 extra_data.insert(
789 "follows".to_string(),
790 JsonValue::String(info.follows[0].clone()),
791 );
792 } else if !info.follows.is_empty() {
793 extra_data.insert(
794 "follows".to_string(),
795 JsonValue::Array(
796 info.follows
797 .iter()
798 .cloned()
799 .map(JsonValue::String)
800 .collect(),
801 ),
802 );
803 }
804 if let Some(flake) = info.flake {
805 extra_data.insert("flake".to_string(), JsonValue::Bool(flake));
806 }
807
808 Dependency {
809 purl: build_nix_purl(&name, None),
810 extracted_requirement: info.requirement,
811 scope: Some("inputs".to_string()),
812 is_runtime: Some(false),
813 is_optional: Some(false),
814 is_pinned: Some(false),
815 is_direct: Some(true),
816 resolved_package: None,
817 extra_data: (!extra_data.is_empty()).then_some(extra_data),
818 }
819 })
820 .collect::<Vec<_>>();
821
822 dependencies.sort_by(|left, right| left.purl.cmp(&right.purl));
823 dependencies
824}
825
826fn collect_input_entries(
827 entries: &[(Vec<String>, Expr)],
828 inputs: &mut HashMap<String, FlakeInputInfo>,
829 current_input: Option<&str>,
830) {
831 for (path, expr) in entries {
832 if let Some(input_name) = current_input {
833 apply_input_field(
834 inputs.entry(input_name.to_string()).or_default(),
835 path,
836 expr,
837 );
838 continue;
839 }
840
841 collect_input_path(path, expr, inputs);
842 }
843}
844
845fn collect_input_path(path: &[String], expr: &Expr, inputs: &mut HashMap<String, FlakeInputInfo>) {
846 let Some(input_name) = path.first() else {
847 return;
848 };
849
850 if path.len() == 1 {
851 match expr {
852 Expr::AttrSet(entries) => collect_input_entries(entries, inputs, Some(input_name)),
853 Expr::String(value) => {
854 inputs.entry(input_name.clone()).or_default().requirement = Some(value.clone())
855 }
856 _ => {}
857 }
858 return;
859 }
860
861 apply_input_field(
862 inputs.entry(input_name.clone()).or_default(),
863 &path[1..],
864 expr,
865 );
866}
867
868fn apply_input_field(info: &mut FlakeInputInfo, path: &[String], expr: &Expr) {
869 if path == ["url"] {
870 info.requirement = expr_as_string(expr);
871 return;
872 }
873
874 if path == ["flake"] {
875 info.flake = expr_as_bool(expr);
876 return;
877 }
878
879 if path.len() == 3
880 && path[0] == "inputs"
881 && path[2] == "follows"
882 && let Some(value) = expr_as_string(expr)
883 {
884 info.follows.push(value);
885 }
886}
887
888fn build_list_dependencies(
889 entries: &[(Vec<String>, Expr)],
890 field_name: &str,
891 runtime: bool,
892) -> Vec<Dependency> {
893 let Some(expr) = find_attr(entries, &[field_name]) else {
894 return Vec::new();
895 };
896 let Some(items) = list_items(expr) else {
897 return Vec::new();
898 };
899
900 items
901 .iter()
902 .flat_map(expr_to_dependency_symbols)
903 .filter_map(|symbol| {
904 let name = symbol.rsplit('.').next()?.to_string();
905 Some(Dependency {
906 purl: build_nix_purl(&name, None),
907 extracted_requirement: None,
908 scope: Some(field_name.to_string()),
909 is_runtime: Some(runtime),
910 is_optional: Some(false),
911 is_pinned: Some(false),
912 is_direct: Some(true),
913 resolved_package: None,
914 extra_data: None,
915 })
916 })
917 .collect()
918}
919
920fn expr_to_dependency_symbols(expr: &Expr) -> Vec<String> {
921 match expr {
922 Expr::Symbol(symbol) => vec![symbol.clone()],
923 Expr::Application(parts) => parts.iter().filter_map(expr_as_symbol).collect(),
924 _ => Vec::new(),
925 }
926}
927
928fn fallback_name(path: &Path) -> Option<String> {
929 path.parent()
930 .and_then(|parent| parent.file_name())
931 .and_then(|name| name.to_str())
932 .map(ToOwned::to_owned)
933}
934
935fn build_nix_purl(name: &str, version: Option<&str>) -> Option<String> {
936 let mut purl = PackageUrl::new(PackageType::Nix.as_str(), name).ok()?;
937 if let Some(version) = version {
938 purl.with_version(version).ok()?;
939 }
940 Some(purl.to_string())
941}
942
943fn parse_nix_expr(content: &str) -> Result<Expr, String> {
944 let tokens = Lexer::new(content).tokenize()?;
945 Parser::new(tokens).parse()
946}
947
948fn attrset_entries(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
949 match expr {
950 Expr::AttrSet(entries) => Some(entries),
951 _ => None,
952 }
953}
954
955fn list_items(expr: &Expr) -> Option<&[Expr]> {
956 match expr {
957 Expr::List(items) => Some(items),
958 _ => None,
959 }
960}
961
962fn expr_as_string(expr: &Expr) -> Option<String> {
963 match expr {
964 Expr::String(value) => Some(value.clone()),
965 Expr::Symbol(value) => Some(value.clone()),
966 _ => None,
967 }
968}
969
970fn expr_as_symbol(expr: &Expr) -> Option<String> {
971 match expr {
972 Expr::Symbol(value) => Some(value.clone()),
973 _ => None,
974 }
975}
976
977fn expr_as_bool(expr: &Expr) -> Option<bool> {
978 match expr {
979 Expr::Symbol(value) if value == "true" => Some(true),
980 Expr::Symbol(value) if value == "false" => Some(false),
981 _ => None,
982 }
983}
984
985fn expr_to_scalar_string(expr: &Expr) -> Option<String> {
986 match expr {
987 Expr::String(value) | Expr::Symbol(value) => Some(value.clone()),
988 Expr::Application(parts) => parts.last().and_then(expr_to_scalar_string),
989 _ => None,
990 }
991}
992
993fn find_attr<'a>(entries: &'a [(Vec<String>, Expr)], path: &[&str]) -> Option<&'a Expr> {
994 for (key, value) in entries {
995 if key.iter().map(String::as_str).eq(path.iter().copied()) {
996 return Some(value);
997 }
998
999 if key.len() < path.len()
1000 && key
1001 .iter()
1002 .map(String::as_str)
1003 .eq(path[..key.len()].iter().copied())
1004 && let Expr::AttrSet(child_entries) = value
1005 && let Some(found) = find_attr(child_entries, &path[key.len()..])
1006 {
1007 return Some(found);
1008 }
1009 }
1010
1011 None
1012}
1013
1014fn find_string_attr(entries: &[(Vec<String>, Expr)], path: &[&str]) -> Option<String> {
1015 find_attr(entries, path).and_then(expr_to_scalar_string)
1016}
1017
1018fn find_mk_derivation_attrset(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1019 match expr {
1020 Expr::Application(parts) => {
1021 let is_derivation = parts
1022 .first()
1023 .and_then(expr_as_symbol)
1024 .is_some_and(|symbol| symbol.ends_with("mkDerivation"));
1025 if is_derivation {
1026 return parts.iter().rev().find_map(attrset_entries);
1027 }
1028 None
1029 }
1030 _ => None,
1031 }
1032}
1033
1034fn split_derivation_name(name: &str) -> (String, Option<String>) {
1035 let mut parts = name.rsplitn(2, '-');
1036 let maybe_version = parts
1037 .next()
1038 .filter(|value| value.chars().any(|ch| ch.is_ascii_digit()));
1039 let maybe_name = parts.next();
1040
1041 match (maybe_name, maybe_version) {
1042 (Some(package_name), Some(version)) => {
1043 (package_name.to_string(), Some(version.to_string()))
1044 }
1045 _ => (name.to_string(), None),
1046 }
1047}
1048
1049fn default_flake_package_data() -> PackageData {
1050 PackageData {
1051 package_type: Some(PackageType::Nix),
1052 primary_language: Some("Nix".to_string()),
1053 datasource_id: Some(DatasourceId::NixFlakeNix),
1054 ..Default::default()
1055 }
1056}
1057
1058fn default_flake_lock_package_data() -> PackageData {
1059 PackageData {
1060 package_type: Some(PackageType::Nix),
1061 primary_language: Some("JSON".to_string()),
1062 datasource_id: Some(DatasourceId::NixFlakeLock),
1063 ..Default::default()
1064 }
1065}
1066
1067fn default_default_nix_package_data() -> PackageData {
1068 PackageData {
1069 package_type: Some(PackageType::Nix),
1070 primary_language: Some("Nix".to_string()),
1071 datasource_id: Some(DatasourceId::NixDefaultNix),
1072 ..Default::default()
1073 }
1074}
1075
1076crate::register_parser!(
1077 "Nix flake manifest",
1078 &["**/flake.nix"],
1079 "nix",
1080 "Nix",
1081 Some("https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-flake.html"),
1082);
1083
1084crate::register_parser!(
1085 "Nix flake lockfile",
1086 &["**/flake.lock"],
1087 "nix",
1088 "JSON",
1089 Some("https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-flake.html"),
1090);
1091
1092crate::register_parser!(
1093 "Nix derivation manifest",
1094 &["**/default.nix"],
1095 "nix",
1096 "Nix",
1097 Some("https://nix.dev/manual/nix/stable/language/derivations.html"),
1098);