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                '{' => {
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);