Skip to main content

provenant/parsers/
nix.rs

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);