Skip to main content

provenant/parsers/
nix.rs

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