Skip to main content

provenant/parsers/
nix.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5use std::path::Path;
6
7use crate::parser_warn as warn;
8use packageurl::PackageUrl;
9use serde_json::Value as JsonValue;
10
11use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
12use crate::parsers::utils::{
13    MAX_ITERATION_COUNT, RecursionGuard, capped_iteration_limit, read_file_to_string,
14    truncate_field,
15};
16
17use super::PackageParser;
18use super::metadata::ParserMetadata;
19
20pub struct NixFlakeLockParser;
21
22impl PackageParser for NixFlakeLockParser {
23    const PACKAGE_TYPE: PackageType = PackageType::Nix;
24
25    fn metadata() -> Vec<ParserMetadata> {
26        vec![ParserMetadata {
27            description: "Nix flake lockfile",
28            file_patterns: &["**/flake.lock"],
29            package_type: "nix",
30            primary_language: "JSON",
31            documentation_url: Some(
32                "https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-flake.html",
33            ),
34        }]
35    }
36
37    fn is_match(path: &Path) -> bool {
38        path.file_name().is_some_and(|name| name == "flake.lock")
39    }
40
41    fn extract_packages(path: &Path) -> Vec<PackageData> {
42        let content = match read_file_to_string(path, None) {
43            Ok(content) => content,
44            Err(error) => {
45                warn!("Failed to read flake.lock at {:?}: {}", path, error);
46                return vec![default_flake_lock_package_data()];
47            }
48        };
49
50        let json: JsonValue = match serde_json::from_str(&content) {
51            Ok(json) => json,
52            Err(error) => {
53                warn!("Failed to parse flake.lock at {:?}: {}", path, error);
54                return vec![default_flake_lock_package_data()];
55            }
56        };
57
58        match parse_flake_lock(path, &json) {
59            Ok(package) => vec![package],
60            Err(error) => {
61                warn!("Failed to interpret flake.lock at {:?}: {}", path, error);
62                vec![default_flake_lock_package_data()]
63            }
64        }
65    }
66}
67
68pub struct NixFlakeParser;
69
70impl PackageParser for NixFlakeParser {
71    const PACKAGE_TYPE: PackageType = PackageType::Nix;
72
73    fn metadata() -> Vec<ParserMetadata> {
74        vec![ParserMetadata {
75            description: "Nix flake manifest",
76            file_patterns: &["**/flake.nix"],
77            package_type: "nix",
78            primary_language: "Nix",
79            documentation_url: Some(
80                "https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-flake.html",
81            ),
82        }]
83    }
84
85    fn is_match(path: &Path) -> bool {
86        path.file_name().is_some_and(|name| name == "flake.nix")
87    }
88
89    fn extract_packages(path: &Path) -> Vec<PackageData> {
90        let content = match read_file_to_string(path, None) {
91            Ok(content) => content,
92            Err(error) => {
93                warn!("Failed to read flake.nix at {:?}: {}", path, error);
94                return vec![default_flake_package_data()];
95            }
96        };
97
98        match parse_flake_nix(path, &content) {
99            Ok(package) => vec![package],
100            Err(_) => vec![default_flake_package_data()],
101        }
102    }
103}
104
105pub struct NixDefaultParser;
106
107impl PackageParser for NixDefaultParser {
108    const PACKAGE_TYPE: PackageType = PackageType::Nix;
109
110    fn metadata() -> Vec<ParserMetadata> {
111        vec![ParserMetadata {
112            description: "Nix derivation manifest",
113            file_patterns: &["**/default.nix"],
114            package_type: "nix",
115            primary_language: "Nix",
116            documentation_url: Some("https://nix.dev/manual/nix/stable/language/derivations.html"),
117        }]
118    }
119
120    fn is_match(path: &Path) -> bool {
121        path.file_name().is_some_and(|name| name == "default.nix")
122    }
123
124    fn extract_packages(path: &Path) -> Vec<PackageData> {
125        let content = match read_file_to_string(path, None) {
126            Ok(content) => content,
127            Err(error) => {
128                warn!("Failed to read default.nix at {:?}: {}", path, error);
129                return vec![default_default_nix_package_data()];
130            }
131        };
132
133        match parse_default_nix(path, &content) {
134            Ok(package) => vec![package],
135            Err(_) => vec![default_default_nix_package_data()],
136        }
137    }
138}
139
140#[derive(Clone, Debug)]
141enum Expr {
142    AttrSet(Vec<(Vec<String>, Expr)>),
143    List(Vec<Expr>),
144    String(String),
145    Symbol(String),
146    Application(Vec<Expr>),
147    Let {
148        bindings: Vec<(Vec<String>, Expr)>,
149        body: Box<Expr>,
150    },
151    Select {
152        target: Box<Expr>,
153        path: Vec<String>,
154    },
155}
156
157type NixAttrEntries = [(Vec<String>, Expr)];
158type NixAttrEntriesRef<'a> = &'a NixAttrEntries;
159type NixScopeStack<'a> = Vec<NixAttrEntriesRef<'a>>;
160
161#[derive(Clone, Debug, PartialEq, Eq)]
162enum Token {
163    LBrace,
164    RBrace,
165    LBracket,
166    RBracket,
167    LParen,
168    RParen,
169    Equals,
170    Semicolon,
171    Colon,
172    Dot,
173    Comma,
174    String(String),
175    Ident(String),
176}
177
178#[derive(Default)]
179struct FlakeInputInfo {
180    requirement: Option<String>,
181    follows: Vec<String>,
182    flake: Option<bool>,
183}
184
185struct Lexer {
186    chars: Vec<char>,
187    index: usize,
188}
189
190impl Lexer {
191    fn new(input: &str) -> Self {
192        Self {
193            chars: input.chars().collect(),
194            index: 0,
195        }
196    }
197
198    fn tokenize(mut self) -> Result<Vec<Token>, String> {
199        let mut tokens = Vec::new();
200
201        while let Some(ch) = self.peek() {
202            if tokens.len() >= MAX_ITERATION_COUNT {
203                warn!("Lexer exceeded MAX_ITERATION_COUNT token limit");
204                break;
205            }
206
207            if ch.is_whitespace() {
208                self.index += 1;
209                continue;
210            }
211
212            if ch == '#' {
213                self.skip_line_comment();
214                continue;
215            }
216
217            if ch == '/' && self.peek_n(1) == Some('*') {
218                self.skip_block_comment()?;
219                continue;
220            }
221
222            match ch {
223                '$' if self.peek_n(1) == Some('{') => {
224                    tokens.push(Token::Ident(self.read_interpolation_literal()?));
225                }
226                '.' if self.peek_n(1) == Some('/') => {
227                    tokens.push(Token::Ident(self.read_path_literal()?));
228                }
229                '.' if self.peek_n(1) == Some('.') && self.peek_n(2) == Some('/') => {
230                    tokens.push(Token::Ident(self.read_path_literal()?));
231                }
232                '{' => {
233                    self.index += 1;
234                    tokens.push(Token::LBrace);
235                }
236                '}' => {
237                    self.index += 1;
238                    tokens.push(Token::RBrace);
239                }
240                '[' => {
241                    self.index += 1;
242                    tokens.push(Token::LBracket);
243                }
244                ']' => {
245                    self.index += 1;
246                    tokens.push(Token::RBracket);
247                }
248                '(' => {
249                    self.index += 1;
250                    tokens.push(Token::LParen);
251                }
252                ')' => {
253                    self.index += 1;
254                    tokens.push(Token::RParen);
255                }
256                '=' => {
257                    self.index += 1;
258                    tokens.push(Token::Equals);
259                }
260                ';' => {
261                    self.index += 1;
262                    tokens.push(Token::Semicolon);
263                }
264                ':' => {
265                    self.index += 1;
266                    tokens.push(Token::Colon);
267                }
268                '.' => {
269                    self.index += 1;
270                    tokens.push(Token::Dot);
271                }
272                ',' => {
273                    self.index += 1;
274                    tokens.push(Token::Comma);
275                }
276                '"' => tokens.push(Token::String(self.read_double_quoted_string()?)),
277                '\'' if self.peek_n(1) == Some('\'') => {
278                    tokens.push(Token::String(self.read_indented_string()?));
279                }
280                _ => tokens.push(Token::Ident(self.read_ident()?)),
281            }
282        }
283
284        Ok(tokens)
285    }
286
287    fn peek(&self) -> Option<char> {
288        self.chars.get(self.index).copied()
289    }
290
291    fn peek_n(&self, offset: usize) -> Option<char> {
292        self.chars.get(self.index + offset).copied()
293    }
294
295    fn skip_line_comment(&mut self) {
296        while let Some(ch) = self.peek() {
297            self.index += 1;
298            if ch == '\n' {
299                break;
300            }
301        }
302    }
303
304    fn skip_block_comment(&mut self) -> Result<(), String> {
305        self.index += 2;
306        while let Some(ch) = self.peek() {
307            if ch == '*' && self.peek_n(1) == Some('/') {
308                self.index += 2;
309                return Ok(());
310            }
311            self.index += 1;
312        }
313        Err("unterminated block comment".to_string())
314    }
315
316    fn read_double_quoted_string(&mut self) -> Result<String, String> {
317        self.index += 1;
318        let mut result = String::new();
319        let mut escaped = false;
320
321        while let Some(ch) = self.peek() {
322            self.index += 1;
323            if escaped {
324                result.push(match ch {
325                    'n' => '\n',
326                    'r' => '\r',
327                    't' => '\t',
328                    '"' => '"',
329                    '\\' => '\\',
330                    other => other,
331                });
332                escaped = false;
333                continue;
334            }
335
336            if ch == '\\' {
337                escaped = true;
338                continue;
339            }
340
341            if ch == '$' && self.peek() == Some('{') {
342                result.push(ch);
343                result.push('{');
344                self.index += 1;
345                let mut interpolation_depth = 1usize;
346
347                while let Some(inner) = self.peek() {
348                    self.index += 1;
349                    result.push(inner);
350
351                    match inner {
352                        '{' => interpolation_depth += 1,
353                        '}' => {
354                            interpolation_depth = interpolation_depth.saturating_sub(1);
355                            if interpolation_depth == 0 {
356                                break;
357                            }
358                        }
359                        _ => {}
360                    }
361                }
362
363                if interpolation_depth != 0 {
364                    return Err("unterminated string interpolation".to_string());
365                }
366
367                continue;
368            }
369
370            if ch == '"' {
371                return Ok(result);
372            }
373
374            result.push(ch);
375        }
376
377        Err("unterminated string".to_string())
378    }
379
380    fn read_path_literal(&mut self) -> Result<String, String> {
381        let start = self.index;
382
383        while let Some(ch) = self.peek() {
384            if ch.is_whitespace()
385                || matches!(
386                    ch,
387                    '{' | '}' | '[' | ']' | '(' | ')' | '=' | ';' | ':' | ',' | '"'
388                )
389                || (ch == '\'' && self.peek_n(1) == Some('\''))
390                || ch == '#'
391            {
392                break;
393            }
394
395            if ch == '/' && self.peek_n(1) == Some('*') {
396                break;
397            }
398
399            self.index += 1;
400        }
401
402        if self.index == start {
403            return Err("unexpected token".to_string());
404        }
405
406        Ok(self.chars[start..self.index].iter().collect())
407    }
408
409    fn read_interpolation_literal(&mut self) -> Result<String, String> {
410        let start = self.index;
411        self.index += 2;
412        let mut depth = 1usize;
413
414        while let Some(ch) = self.peek() {
415            self.index += 1;
416
417            match ch {
418                '{' => depth += 1,
419                '}' => {
420                    depth = depth.saturating_sub(1);
421                    if depth == 0 {
422                        return Ok(self.chars[start..self.index].iter().collect());
423                    }
424                }
425                _ => {}
426            }
427        }
428
429        Err("unterminated interpolation literal".to_string())
430    }
431
432    fn read_indented_string(&mut self) -> Result<String, String> {
433        self.index += 2;
434        let mut result = String::new();
435
436        while let Some(ch) = self.peek() {
437            if ch == '\'' && self.peek_n(1) == Some('\'') {
438                self.index += 2;
439                return Ok(result);
440            }
441            result.push(ch);
442            self.index += 1;
443        }
444
445        Err("unterminated indented string".to_string())
446    }
447
448    fn read_ident(&mut self) -> Result<String, String> {
449        let start = self.index;
450
451        while let Some(ch) = self.peek() {
452            if ch.is_whitespace()
453                || matches!(
454                    ch,
455                    '{' | '}' | '[' | ']' | '(' | ')' | '=' | ';' | ':' | ',' | '.' | '"'
456                )
457                || (ch == '\'' && self.peek_n(1) == Some('\''))
458                || ch == '#'
459            {
460                break;
461            }
462
463            if ch == '/' && self.peek_n(1) == Some('*') {
464                break;
465            }
466
467            self.index += 1;
468        }
469
470        if self.index == start {
471            return Err("unexpected token".to_string());
472        }
473
474        Ok(self.chars[start..self.index].iter().collect())
475    }
476}
477
478struct Parser {
479    tokens: Vec<Token>,
480    index: usize,
481    guard: RecursionGuard<()>,
482}
483
484impl Parser {
485    fn new(tokens: Vec<Token>) -> Self {
486        Self {
487            tokens,
488            index: 0,
489            guard: RecursionGuard::depth_only(),
490        }
491    }
492
493    fn parse(mut self) -> Result<Expr, String> {
494        let expr = self.parse_expr()?;
495        if self.peek().is_some() {
496            return Err("unexpected trailing tokens".to_string());
497        }
498        Ok(expr)
499    }
500
501    fn parse_expr(&mut self) -> Result<Expr, String> {
502        if self.guard.descend() {
503            return Err("recursion depth exceeded".to_string());
504        }
505
506        if self.peek() == Some(&Token::LBrace) && self.looks_like_lambda_binder_set()? {
507            self.skip_lambda_binder_set()?;
508            self.expect(&Token::Colon)?;
509            let result = self.parse_expr();
510            self.guard.ascend();
511            return result;
512        }
513
514        if self.looks_like_prefixed_lambda_binder_set()? {
515            self.index += 1;
516            self.skip_lambda_binder_set()?;
517            self.expect(&Token::Colon)?;
518            let result = self.parse_expr();
519            self.guard.ascend();
520            return result;
521        }
522
523        let first = self.parse_term()?;
524        if self.consume(&Token::Colon) {
525            let result = self.parse_expr();
526            self.guard.ascend();
527            return result;
528        }
529
530        let mut terms = vec![first];
531        while self.can_start_term() {
532            terms.push(self.parse_term()?);
533        }
534
535        let expr = if terms.len() == 1 {
536            terms
537                .into_iter()
538                .next()
539                .unwrap_or_else(|| Expr::Symbol(String::new()))
540        } else {
541            Expr::Application(terms)
542        };
543
544        let result = self.parse_postfix(expr);
545        self.guard.ascend();
546        result
547    }
548
549    fn parse_postfix(&mut self, mut expr: Expr) -> Result<Expr, String> {
550        while self.consume(&Token::Dot) {
551            let mut path = vec![self.take_attr_key()?];
552            while self.consume(&Token::Dot) {
553                path.push(self.take_attr_key()?);
554            }
555            expr = Expr::Select {
556                target: Box::new(expr),
557                path,
558            };
559        }
560
561        Ok(expr)
562    }
563
564    fn parse_term(&mut self) -> Result<Expr, String> {
565        match self.peek() {
566            Some(Token::Ident(keyword)) if keyword == "let" => self.parse_let_in_expr(),
567            Some(Token::Ident(keyword)) if keyword == "with" => {
568                self.index += 1;
569                let _ = self.parse_expr()?;
570                self.expect(&Token::Semicolon)?;
571                self.parse_expr()
572            }
573            Some(Token::Ident(keyword)) if keyword == "rec" => {
574                if matches!(self.peek_n(1), Some(Token::LBrace)) {
575                    self.index += 1;
576                    self.parse_attrset()
577                } else {
578                    self.parse_symbol()
579                }
580            }
581            Some(Token::LBrace) => self.parse_attrset(),
582            Some(Token::LBracket) => self.parse_list(),
583            Some(Token::LParen) => {
584                self.index += 1;
585                let expr = self.parse_expr()?;
586                self.expect(&Token::RParen)?;
587                Ok(expr)
588            }
589            Some(Token::String(_)) => self.parse_string(),
590            Some(Token::Ident(_)) => self.parse_symbol(),
591            _ => Err("expected expression".to_string()),
592        }
593    }
594
595    fn parse_let_in_expr(&mut self) -> Result<Expr, String> {
596        self.take_exact_ident("let")?;
597        let mut bindings = Vec::new();
598
599        while !matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "in") {
600            if self.peek().is_none() {
601                return Err("unterminated let expression".to_string());
602            }
603
604            if bindings.len() >= MAX_ITERATION_COUNT {
605                warn!("parse_let_in_expr exceeded MAX_ITERATION_COUNT bindings limit");
606                break;
607            }
608
609            if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
610                bindings.extend(self.parse_inherit_entries()?);
611                continue;
612            }
613
614            let key = self.parse_attr_path()?;
615            self.expect(&Token::Equals)?;
616            let value = self.parse_expr()?;
617            self.expect(&Token::Semicolon)?;
618            bindings.push((key, value));
619        }
620
621        self.take_exact_ident("in")?;
622        let body = self.parse_expr()?;
623        Ok(Expr::Let {
624            bindings,
625            body: Box::new(body),
626        })
627    }
628
629    fn parse_attrset(&mut self) -> Result<Expr, String> {
630        self.expect(&Token::LBrace)?;
631        let mut entries = Vec::new();
632
633        loop {
634            if self.consume(&Token::RBrace) {
635                return Ok(Expr::AttrSet(entries));
636            }
637
638            if self.peek().is_none() {
639                return Err("unterminated attribute set".to_string());
640            }
641
642            if entries.len() >= MAX_ITERATION_COUNT {
643                warn!("parse_attrset exceeded MAX_ITERATION_COUNT entries limit");
644                break;
645            }
646
647            if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
648                entries.extend(self.parse_inherit_entries()?);
649                continue;
650            }
651
652            let key = self.parse_attr_path()?;
653            self.expect(&Token::Equals)?;
654            let value = self.parse_expr()?;
655            self.expect(&Token::Semicolon)?;
656            entries.push((key, value));
657        }
658
659        Ok(Expr::AttrSet(entries))
660    }
661
662    fn parse_attr_path(&mut self) -> Result<Vec<String>, String> {
663        let mut path = vec![self.take_attr_key()?];
664        while self.consume(&Token::Dot) {
665            path.push(self.take_attr_key()?);
666        }
667        Ok(path)
668    }
669
670    fn parse_inherit_entries(&mut self) -> Result<Vec<(Vec<String>, Expr)>, String> {
671        self.take_exact_ident("inherit")?;
672
673        let inherit_from = if self.consume(&Token::LParen) {
674            let expr = self.parse_expr()?;
675            self.expect(&Token::RParen)?;
676            Some(expr)
677        } else {
678            None
679        };
680
681        let mut entries = Vec::new();
682        while !self.consume(&Token::Semicolon) {
683            if self.peek().is_none() {
684                return Err("unterminated inherit statement".to_string());
685            }
686
687            if entries.len() >= MAX_ITERATION_COUNT {
688                warn!("parse_inherit_entries exceeded MAX_ITERATION_COUNT entries limit");
689                break;
690            }
691
692            let name = self.take_attr_key()?;
693            let value = match &inherit_from {
694                Some(source) => Expr::Select {
695                    target: Box::new(source.clone()),
696                    path: vec![name.clone()],
697                },
698                None => Expr::Symbol(name.clone()),
699            };
700            entries.push((vec![name], value));
701        }
702
703        Ok(entries)
704    }
705
706    fn parse_list(&mut self) -> Result<Expr, String> {
707        self.expect(&Token::LBracket)?;
708        let mut items = Vec::new();
709        while !self.consume(&Token::RBracket) {
710            if self.peek().is_none() {
711                return Err("unterminated list".to_string());
712            }
713
714            if items.len() >= MAX_ITERATION_COUNT {
715                warn!("parse_list exceeded MAX_ITERATION_COUNT items limit");
716                break;
717            }
718
719            items.push(self.parse_expr()?);
720        }
721        Ok(Expr::List(items))
722    }
723
724    fn parse_string(&mut self) -> Result<Expr, String> {
725        match self.next() {
726            Some(Token::String(value)) => Ok(Expr::String(value)),
727            _ => Err("expected string".to_string()),
728        }
729    }
730
731    fn parse_symbol(&mut self) -> Result<Expr, String> {
732        let mut parts = vec![self.take_ident()?];
733        while self.consume(&Token::Dot) {
734            parts.push(self.take_ident()?);
735        }
736        Ok(Expr::Symbol(parts.join(".")))
737    }
738
739    fn take_ident(&mut self) -> Result<String, String> {
740        match self.next() {
741            Some(Token::Ident(value)) => Ok(value),
742            _ => Err("expected identifier".to_string()),
743        }
744    }
745
746    fn take_exact_ident(&mut self, expected: &str) -> Result<(), String> {
747        match self.next() {
748            Some(Token::Ident(value)) if value == expected => Ok(()),
749            _ => Err(format!("expected {expected}")),
750        }
751    }
752
753    fn take_attr_key(&mut self) -> Result<String, String> {
754        match self.next() {
755            Some(Token::Ident(value)) | Some(Token::String(value)) => Ok(value),
756            _ => Err("expected attribute key".to_string()),
757        }
758    }
759
760    fn can_start_term(&self) -> bool {
761        matches!(
762            self.peek(),
763            Some(Token::LBrace)
764                | Some(Token::LBracket)
765                | Some(Token::LParen)
766                | Some(Token::String(_))
767                | Some(Token::Ident(_))
768        )
769    }
770
771    fn looks_like_lambda_binder_set(&self) -> Result<bool, String> {
772        if self.peek() != Some(&Token::LBrace) {
773            return Ok(false);
774        }
775
776        self.looks_like_lambda_binder_set_from(self.index)
777    }
778
779    fn looks_like_prefixed_lambda_binder_set(&self) -> Result<bool, String> {
780        match (self.peek(), self.peek_n(1)) {
781            (Some(Token::Ident(prefix)), Some(Token::LBrace)) if prefix.ends_with('@') => {
782                self.looks_like_lambda_binder_set_from(self.index + 1)
783            }
784            _ => Ok(false),
785        }
786    }
787
788    fn looks_like_lambda_binder_set_from(&self, start_index: usize) -> Result<bool, String> {
789        if self.tokens.get(start_index) != Some(&Token::LBrace) {
790            return Ok(false);
791        }
792
793        let mut depth = 0usize;
794        let mut index = start_index;
795
796        while let Some(token) = self.tokens.get(index) {
797            match token {
798                Token::LBrace => depth += 1,
799                Token::RBrace => {
800                    depth = depth.saturating_sub(1);
801                    if depth == 0 {
802                        return Ok(matches!(self.tokens.get(index + 1), Some(Token::Colon)));
803                    }
804                }
805                Token::Equals | Token::Semicolon if depth == 1 => return Ok(false),
806                _ => {}
807            }
808
809            index += 1;
810        }
811
812        Err("unterminated lambda binder set".to_string())
813    }
814
815    fn skip_lambda_binder_set(&mut self) -> Result<(), String> {
816        self.expect(&Token::LBrace)?;
817        let mut depth = 1usize;
818
819        while depth > 0 {
820            match self.next() {
821                Some(Token::LBrace) => depth += 1,
822                Some(Token::RBrace) => depth = depth.saturating_sub(1),
823                Some(_) => {}
824                None => return Err("unterminated lambda binder set".to_string()),
825            }
826        }
827
828        Ok(())
829    }
830
831    fn expect(&mut self, expected: &Token) -> Result<(), String> {
832        if self.consume(expected) {
833            Ok(())
834        } else {
835            Err(format!("expected {:?}", expected))
836        }
837    }
838
839    fn consume(&mut self, expected: &Token) -> bool {
840        if self.peek() == Some(expected) {
841            self.index += 1;
842            true
843        } else {
844            false
845        }
846    }
847
848    fn peek(&self) -> Option<&Token> {
849        self.tokens.get(self.index)
850    }
851
852    fn peek_n(&self, offset: usize) -> Option<&Token> {
853        self.tokens.get(self.index + offset)
854    }
855
856    fn next(&mut self) -> Option<Token> {
857        let token = self.tokens.get(self.index).cloned();
858        if token.is_some() {
859            self.index += 1;
860        }
861        token
862    }
863}
864
865fn parse_flake_nix(path: &Path, content: &str) -> Result<PackageData, String> {
866    let expr = parse_nix_expr(content)?;
867    let scopes = Vec::new();
868    let (root, scopes) =
869        root_attrset_with_scopes(&expr, &scopes, &mut RecursionGuard::depth_only())
870            .ok_or_else(|| "flake.nix root was not an attribute set".to_string())?;
871
872    let mut package = default_flake_package_data();
873    package.name = fallback_name(path).map(truncate_field);
874    package.description =
875        find_string_attr_with_scopes(root, &["description"], &scopes).map(truncate_field);
876    package.purl = package
877        .name
878        .as_deref()
879        .and_then(|name| build_nix_purl(name, None));
880    package.dependencies = build_flake_dependencies(root, &scopes);
881
882    Ok(package)
883}
884
885fn parse_default_nix(path: &Path, content: &str) -> Result<PackageData, String> {
886    match parse_nix_expr(content) {
887        Ok(expr) => extract_default_nix_package(path, &expr, &Vec::new(), 0)
888            .or_else(|_| extract_flake_compat_default_package_from_content(path, content)),
889        Err(parse_error) => extract_flake_compat_default_package_from_content(path, content)
890            .map_err(|_| parse_error),
891    }
892}
893
894fn parse_flake_lock(path: &Path, json: &JsonValue) -> Result<PackageData, String> {
895    let version = json
896        .get("version")
897        .and_then(JsonValue::as_i64)
898        .ok_or_else(|| "flake.lock missing integer version".to_string())?;
899    let root = json
900        .get("root")
901        .and_then(JsonValue::as_str)
902        .ok_or_else(|| "flake.lock missing root".to_string())?;
903    let nodes = json
904        .get("nodes")
905        .and_then(JsonValue::as_object)
906        .ok_or_else(|| "flake.lock missing nodes".to_string())?;
907    let root_node = nodes
908        .get(root)
909        .and_then(JsonValue::as_object)
910        .ok_or_else(|| "flake.lock root node missing".to_string())?;
911    let root_inputs = root_node
912        .get("inputs")
913        .and_then(JsonValue::as_object)
914        .ok_or_else(|| "flake.lock root node missing inputs".to_string())?;
915
916    let mut package = default_flake_lock_package_data();
917    package.name = fallback_name(path).map(truncate_field);
918    package.purl = package
919        .name
920        .as_deref()
921        .and_then(|name| build_nix_purl(name, None));
922
923    let mut extra_data = HashMap::new();
924    extra_data.insert("lock_version".to_string(), JsonValue::from(version));
925    extra_data.insert("root".to_string(), JsonValue::String(root.to_string()));
926    package.extra_data = Some(extra_data);
927
928    let root_inputs_limit =
929        capped_iteration_limit(root_inputs.len(), "nix: flake.lock root inputs");
930    package.dependencies = root_inputs
931        .iter()
932        .take(root_inputs_limit)
933        .filter_map(|(input_name, node_ref)| build_lock_dependency(input_name, node_ref, nodes))
934        .collect();
935    package
936        .dependencies
937        .sort_by(|left, right| left.purl.cmp(&right.purl));
938
939    Ok(package)
940}
941
942fn build_lock_dependency(
943    input_name: &str,
944    node_ref: &JsonValue,
945    nodes: &serde_json::Map<String, JsonValue>,
946) -> Option<Dependency> {
947    let node_id = node_ref.as_str()?;
948    let node = nodes.get(node_id)?.as_object()?;
949    let locked = node.get("locked").and_then(JsonValue::as_object)?;
950    let revision = locked.get("rev").and_then(JsonValue::as_str);
951
952    let mut extra_data = HashMap::new();
953    for key in [
954        "type",
955        "owner",
956        "repo",
957        "narHash",
958        "lastModified",
959        "revCount",
960        "url",
961        "path",
962        "dir",
963        "host",
964    ] {
965        if let Some(value) = locked.get(key) {
966            extra_data.insert(normalize_extra_key(key), value.clone());
967        }
968    }
969    if let Some(value) = node.get("flake").and_then(JsonValue::as_bool) {
970        extra_data.insert("flake".to_string(), JsonValue::Bool(value));
971    }
972    if let Some(original) = node.get("original").and_then(JsonValue::as_object) {
973        if let Some(value) = original.get("type") {
974            extra_data.insert("original_type".to_string(), value.clone());
975        }
976        if let Some(value) = original.get("id") {
977            extra_data.insert("original_id".to_string(), value.clone());
978        }
979        if let Some(value) = original.get("ref") {
980            extra_data.insert("original_ref".to_string(), value.clone());
981        }
982    }
983
984    Some(Dependency {
985        purl: build_nix_purl(input_name, revision),
986        extracted_requirement: build_locked_requirement(locked, node.get("original"))
987            .map(truncate_field),
988        scope: Some("inputs".to_string()),
989        is_runtime: Some(false),
990        is_optional: Some(false),
991        is_pinned: Some(revision.is_some()),
992        is_direct: Some(true),
993        resolved_package: None,
994        extra_data: (!extra_data.is_empty()).then_some(extra_data),
995    })
996}
997
998fn build_locked_requirement(
999    locked: &serde_json::Map<String, JsonValue>,
1000    original: Option<&JsonValue>,
1001) -> Option<String> {
1002    let source_type = locked.get("type").and_then(JsonValue::as_str).or_else(|| {
1003        original
1004            .and_then(|value| value.get("type"))
1005            .and_then(JsonValue::as_str)
1006    });
1007
1008    match source_type {
1009        Some("github") => {
1010            let owner = locked.get("owner").and_then(JsonValue::as_str)?;
1011            let repo = locked.get("repo").and_then(JsonValue::as_str)?;
1012            Some(format!("github:{owner}/{repo}"))
1013        }
1014        Some("indirect") => original
1015            .and_then(|value| value.get("id"))
1016            .and_then(JsonValue::as_str)
1017            .map(ToOwned::to_owned),
1018        _ => locked
1019            .get("url")
1020            .and_then(JsonValue::as_str)
1021            .map(ToOwned::to_owned),
1022    }
1023}
1024
1025fn normalize_extra_key(key: &str) -> String {
1026    match key {
1027        "type" => "source_type".to_string(),
1028        "narHash" => "nar_hash".to_string(),
1029        "lastModified" => "last_modified".to_string(),
1030        "revCount" => "rev_count".to_string(),
1031        other => other.to_string(),
1032    }
1033}
1034
1035fn build_flake_dependencies(
1036    root: &[(Vec<String>, Expr)],
1037    scopes: &[&[(Vec<String>, Expr)]],
1038) -> Vec<Dependency> {
1039    let mut inputs: HashMap<String, FlakeInputInfo> = HashMap::new();
1040
1041    for (path, expr) in root {
1042        if path.first().map(String::as_str) != Some("inputs") {
1043            continue;
1044        }
1045
1046        if path.len() == 1 {
1047            if let Some(entries) = attrset_entries(expr) {
1048                collect_input_entries(entries, scopes, &mut inputs, None);
1049            }
1050            continue;
1051        }
1052
1053        collect_input_path(&path[1..], expr, scopes, &mut inputs);
1054    }
1055
1056    let mut dependencies = inputs
1057        .into_iter()
1058        .map(|(name, info)| {
1059            let mut extra_data = HashMap::new();
1060            if info.follows.len() == 1 {
1061                extra_data.insert(
1062                    "follows".to_string(),
1063                    JsonValue::String(info.follows[0].clone()),
1064                );
1065            } else if !info.follows.is_empty() {
1066                extra_data.insert(
1067                    "follows".to_string(),
1068                    JsonValue::Array(
1069                        info.follows
1070                            .iter()
1071                            .cloned()
1072                            .map(JsonValue::String)
1073                            .collect(),
1074                    ),
1075                );
1076            }
1077            if let Some(flake) = info.flake {
1078                extra_data.insert("flake".to_string(), JsonValue::Bool(flake));
1079            }
1080
1081            Dependency {
1082                purl: build_nix_purl(&name, None),
1083                extracted_requirement: info.requirement.map(truncate_field),
1084                scope: Some("inputs".to_string()),
1085                is_runtime: Some(false),
1086                is_optional: Some(false),
1087                is_pinned: Some(false),
1088                is_direct: Some(true),
1089                resolved_package: None,
1090                extra_data: (!extra_data.is_empty()).then_some(extra_data),
1091            }
1092        })
1093        .collect::<Vec<_>>();
1094
1095    dependencies.sort_by(|left, right| left.purl.cmp(&right.purl));
1096    dependencies
1097}
1098
1099fn collect_input_entries(
1100    entries: &[(Vec<String>, Expr)],
1101    scopes: &[&[(Vec<String>, Expr)]],
1102    inputs: &mut HashMap<String, FlakeInputInfo>,
1103    current_input: Option<&str>,
1104) {
1105    for (path, expr) in entries {
1106        if let Some(input_name) = current_input {
1107            apply_input_field(
1108                inputs.entry(input_name.to_string()).or_default(),
1109                path,
1110                expr,
1111                scopes,
1112            );
1113            continue;
1114        }
1115
1116        collect_input_path(path, expr, scopes, inputs);
1117    }
1118}
1119
1120fn collect_input_path(
1121    path: &[String],
1122    expr: &Expr,
1123    scopes: &[&[(Vec<String>, Expr)]],
1124    inputs: &mut HashMap<String, FlakeInputInfo>,
1125) {
1126    let Some(input_name) = path.first() else {
1127        return;
1128    };
1129
1130    if path.len() == 1 {
1131        match expr {
1132            Expr::AttrSet(entries) => {
1133                collect_input_entries(entries, scopes, inputs, Some(input_name))
1134            }
1135            Expr::String(value) => {
1136                inputs.entry(input_name.clone()).or_default().requirement = Some(value.clone())
1137            }
1138            Expr::Symbol(value) => {
1139                inputs.entry(input_name.clone()).or_default().requirement =
1140                    expr_as_string_with_scopes(
1141                        &Expr::Symbol(value.clone()),
1142                        scopes,
1143                        &mut RecursionGuard::depth_only(),
1144                    )
1145            }
1146            _ => {}
1147        }
1148        return;
1149    }
1150
1151    apply_input_field(
1152        inputs.entry(input_name.clone()).or_default(),
1153        &path[1..],
1154        expr,
1155        scopes,
1156    );
1157}
1158
1159fn apply_input_field(
1160    info: &mut FlakeInputInfo,
1161    path: &[String],
1162    expr: &Expr,
1163    scopes: &[&[(Vec<String>, Expr)]],
1164) {
1165    if path == ["url"] {
1166        info.requirement =
1167            expr_as_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only());
1168        return;
1169    }
1170
1171    if path == ["flake"] {
1172        info.flake = expr_as_bool_with_scopes(expr, scopes, &mut RecursionGuard::depth_only());
1173        return;
1174    }
1175
1176    if path.len() == 3
1177        && path[0] == "inputs"
1178        && path[2] == "follows"
1179        && let Some(value) =
1180            expr_as_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1181    {
1182        info.follows.push(value);
1183    }
1184}
1185
1186fn build_list_dependencies(
1187    entries: &[(Vec<String>, Expr)],
1188    field_name: &str,
1189    runtime: bool,
1190    scopes: &[&[(Vec<String>, Expr)]],
1191) -> Vec<Dependency> {
1192    let Some(expr) = find_attr(entries, &[field_name], &mut RecursionGuard::depth_only()) else {
1193        return Vec::new();
1194    };
1195    let Some(items) = list_items_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1196    else {
1197        return Vec::new();
1198    };
1199
1200    let items_limit = capped_iteration_limit(items.len(), "nix: dependency list items");
1201    items
1202        .iter()
1203        .take(items_limit)
1204        .flat_map(|expr| {
1205            expr_to_dependency_symbols_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1206        })
1207        .filter_map(|symbol| {
1208            let name = symbol.rsplit('.').next()?.to_string();
1209            Some(Dependency {
1210                purl: build_nix_purl(&name, None),
1211                extracted_requirement: None,
1212                scope: Some(field_name.to_string()),
1213                is_runtime: Some(runtime),
1214                is_optional: Some(false),
1215                is_pinned: Some(false),
1216                is_direct: Some(true),
1217                resolved_package: None,
1218                extra_data: None,
1219            })
1220        })
1221        .collect()
1222}
1223
1224fn expr_to_dependency_symbols_with_scopes(
1225    expr: &Expr,
1226    scopes: &[&[(Vec<String>, Expr)]],
1227    guard: &mut RecursionGuard<()>,
1228) -> Vec<String> {
1229    if guard.descend() {
1230        warn!("expr_to_dependency_symbols_with_scopes exceeded MAX_RECURSION_DEPTH");
1231        return Vec::new();
1232    }
1233
1234    let result = match expr {
1235        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only())
1236            .map(|resolved| expr_to_dependency_symbols_with_scopes(resolved, scopes, guard))
1237            .unwrap_or_else(|| vec![symbol.clone()]),
1238        Expr::Application(parts) => parts
1239            .iter()
1240            .filter_map(|expr| {
1241                expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1242            })
1243            .collect(),
1244        Expr::Let { bindings, body } => {
1245            let scopes = extend_scopes(scopes, bindings);
1246            expr_to_dependency_symbols_with_scopes(body, &scopes, guard)
1247        }
1248        Expr::Select { .. } => {
1249            expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1250                .into_iter()
1251                .collect()
1252        }
1253        _ => Vec::new(),
1254    };
1255    guard.ascend();
1256    result
1257}
1258
1259fn fallback_name(path: &Path) -> Option<String> {
1260    path.parent()
1261        .and_then(|parent| parent.file_name())
1262        .and_then(|name| name.to_str())
1263        .map(ToOwned::to_owned)
1264}
1265
1266fn build_nix_purl(name: &str, version: Option<&str>) -> Option<String> {
1267    let mut purl = PackageUrl::new(PackageType::Nix.as_str(), name).ok()?;
1268    if let Some(version) = version {
1269        purl.with_version(version).ok()?;
1270    }
1271    Some(truncate_field(purl.to_string()))
1272}
1273
1274fn parse_nix_expr(content: &str) -> Result<Expr, String> {
1275    let tokens = Lexer::new(content).tokenize()?;
1276    Parser::new(tokens).parse()
1277}
1278
1279fn attrset_entries(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1280    match expr {
1281        Expr::AttrSet(entries) => Some(entries),
1282        _ => None,
1283    }
1284}
1285
1286fn list_items_with_scopes<'a>(
1287    expr: &'a Expr,
1288    scopes: &[&'a [(Vec<String>, Expr)]],
1289    guard: &mut RecursionGuard<()>,
1290) -> Option<&'a [Expr]> {
1291    if guard.descend() {
1292        warn!("list_items_with_scopes exceeded MAX_RECURSION_DEPTH");
1293        return None;
1294    }
1295
1296    let result = match expr {
1297        Expr::List(items) => Some(items.as_slice()),
1298        Expr::Let { bindings, body } => {
1299            let scopes = extend_scopes(scopes, bindings);
1300            list_items_with_scopes(body, &scopes, guard)
1301        }
1302        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only())
1303            .and_then(|resolved| list_items_with_scopes(resolved, scopes, guard)),
1304        Expr::Select { target, path } => {
1305            resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1306                .and_then(|resolved| list_items_with_scopes(resolved, scopes, guard))
1307        }
1308        _ => None,
1309    };
1310    guard.ascend();
1311    result
1312}
1313
1314fn expr_as_symbol(expr: &Expr) -> Option<String> {
1315    match expr {
1316        Expr::Symbol(value) => Some(value.clone()),
1317        _ => None,
1318    }
1319}
1320
1321fn expr_as_symbol_with_scopes(
1322    expr: &Expr,
1323    scopes: &[&[(Vec<String>, Expr)]],
1324    guard: &mut RecursionGuard<()>,
1325) -> Option<String> {
1326    if guard.descend() {
1327        warn!("expr_as_symbol_with_scopes exceeded MAX_RECURSION_DEPTH");
1328        return None;
1329    }
1330
1331    let result = match expr {
1332        Expr::Symbol(value) => resolve_symbol(value, scopes, &mut RecursionGuard::depth_only())
1333            .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes, guard))
1334            .or_else(|| Some(value.clone())),
1335        Expr::Select { target, path } => {
1336            resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1337                .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes, guard))
1338        }
1339        Expr::Let { bindings, body } => {
1340            let scopes = extend_scopes(scopes, bindings);
1341            expr_as_symbol_with_scopes(body, &scopes, guard)
1342        }
1343        _ => expr_as_symbol(expr),
1344    };
1345    guard.ascend();
1346    result
1347}
1348
1349fn expr_as_bool(expr: &Expr) -> Option<bool> {
1350    match expr {
1351        Expr::Symbol(value) if value == "true" => Some(true),
1352        Expr::Symbol(value) if value == "false" => Some(false),
1353        _ => None,
1354    }
1355}
1356
1357fn expr_as_bool_with_scopes(
1358    expr: &Expr,
1359    scopes: &[&[(Vec<String>, Expr)]],
1360    guard: &mut RecursionGuard<()>,
1361) -> Option<bool> {
1362    if guard.descend() {
1363        warn!("expr_as_bool_with_scopes exceeded MAX_RECURSION_DEPTH");
1364        return None;
1365    }
1366
1367    let result = match expr {
1368        Expr::Let { bindings, body } => {
1369            let scopes = extend_scopes(scopes, bindings);
1370            expr_as_bool_with_scopes(body, &scopes, guard)
1371        }
1372        Expr::Symbol(value) => resolve_symbol(value, scopes, &mut RecursionGuard::depth_only())
1373            .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes, guard))
1374            .or_else(|| expr_as_bool(expr)),
1375        Expr::Select { target, path } => {
1376            resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1377                .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes, guard))
1378        }
1379        _ => expr_as_bool(expr),
1380    };
1381    guard.ascend();
1382    result
1383}
1384
1385fn expr_as_string_with_scopes(
1386    expr: &Expr,
1387    scopes: &[&[(Vec<String>, Expr)]],
1388    guard: &mut RecursionGuard<()>,
1389) -> Option<String> {
1390    if guard.descend() {
1391        warn!("expr_as_string_with_scopes exceeded MAX_RECURSION_DEPTH");
1392        return None;
1393    }
1394
1395    let result = match expr {
1396        Expr::String(value) => Some(interpolate_string(value, scopes)),
1397        Expr::Symbol(value) => resolve_symbol(value, scopes, &mut RecursionGuard::depth_only())
1398            .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes, guard))
1399            .or_else(|| Some(value.clone())),
1400        Expr::Application(parts) => parts
1401            .last()
1402            .and_then(|expr| expr_as_string_with_scopes(expr, scopes, guard)),
1403        Expr::Let { bindings, body } => {
1404            let scopes = extend_scopes(scopes, bindings);
1405            expr_as_string_with_scopes(body, &scopes, guard)
1406        }
1407        Expr::Select { target, path } => {
1408            resolve_select(target, path, scopes, &mut RecursionGuard::depth_only())
1409                .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes, guard))
1410        }
1411        _ => None,
1412    };
1413    guard.ascend();
1414    result
1415}
1416
1417fn expr_to_scalar_string_with_scopes(
1418    expr: &Expr,
1419    scopes: &[&[(Vec<String>, Expr)]],
1420    guard: &mut RecursionGuard<()>,
1421) -> Option<String> {
1422    if guard.descend() {
1423        warn!("expr_to_scalar_string_with_scopes exceeded MAX_RECURSION_DEPTH");
1424        return None;
1425    }
1426
1427    let result = match expr {
1428        Expr::Application(parts) => parts
1429            .last()
1430            .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes, guard)),
1431        _ => expr_as_string_with_scopes(expr, scopes, guard),
1432    };
1433    guard.ascend();
1434    result
1435}
1436
1437fn find_attr<'a>(
1438    entries: &'a [(Vec<String>, Expr)],
1439    path: &[&str],
1440    guard: &mut RecursionGuard<()>,
1441) -> Option<&'a Expr> {
1442    if guard.descend() {
1443        warn!("find_attr exceeded MAX_RECURSION_DEPTH");
1444        return None;
1445    }
1446
1447    let result = entries.iter().find_map(|(key, value)| {
1448        if key.iter().map(String::as_str).eq(path.iter().copied()) {
1449            return Some(value);
1450        }
1451
1452        if key.len() < path.len()
1453            && key
1454                .iter()
1455                .map(String::as_str)
1456                .eq(path[..key.len()].iter().copied())
1457            && let Expr::AttrSet(child_entries) = value
1458            && let Some(found) = find_attr(child_entries, &path[key.len()..], guard)
1459        {
1460            return Some(found);
1461        }
1462
1463        None
1464    });
1465
1466    guard.ascend();
1467    result
1468}
1469
1470fn find_string_attr_with_scopes(
1471    entries: &[(Vec<String>, Expr)],
1472    path: &[&str],
1473    scopes: &[&[(Vec<String>, Expr)]],
1474) -> Option<String> {
1475    find_attr(entries, path, &mut RecursionGuard::depth_only())
1476        .and_then(|expr| {
1477            expr_to_scalar_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1478        })
1479        .map(truncate_field)
1480}
1481
1482fn find_mk_derivation_attrset(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1483    match expr {
1484        Expr::Application(parts) => {
1485            let is_derivation = parts
1486                .first()
1487                .and_then(expr_as_symbol)
1488                .is_some_and(|symbol| symbol.ends_with("mkDerivation"));
1489            if is_derivation {
1490                return parts.iter().rev().find_map(attrset_entries);
1491            }
1492            None
1493        }
1494        _ => None,
1495    }
1496}
1497
1498fn extend_scopes<'a>(
1499    scopes: &[NixAttrEntriesRef<'a>],
1500    bindings: NixAttrEntriesRef<'a>,
1501) -> NixScopeStack<'a> {
1502    let mut extended = scopes.to_vec();
1503    extended.push(bindings);
1504    extended
1505}
1506
1507fn root_attrset_with_scopes<'a>(
1508    expr: &'a Expr,
1509    scopes: &[NixAttrEntriesRef<'a>],
1510    guard: &mut RecursionGuard<()>,
1511) -> Option<(NixAttrEntriesRef<'a>, NixScopeStack<'a>)> {
1512    if guard.descend() {
1513        warn!("root_attrset_with_scopes exceeded MAX_RECURSION_DEPTH");
1514        return None;
1515    }
1516
1517    let result = match expr {
1518        Expr::AttrSet(entries) => Some((entries.as_slice(), scopes.to_vec())),
1519        Expr::Let { bindings, body } => {
1520            let scopes = extend_scopes(scopes, bindings);
1521            root_attrset_with_scopes(body, &scopes, guard)
1522        }
1523        _ => None,
1524    };
1525    guard.ascend();
1526    result
1527}
1528
1529fn lookup_binding<'a>(scopes: &[NixAttrEntriesRef<'a>], name: &str) -> Option<&'a Expr> {
1530    scopes
1531        .iter()
1532        .rev()
1533        .find_map(|bindings| find_attr(bindings, &[name], &mut RecursionGuard::depth_only()))
1534}
1535
1536fn resolve_symbol<'a>(
1537    symbol: &str,
1538    scopes: &[NixAttrEntriesRef<'a>],
1539    guard: &mut RecursionGuard<()>,
1540) -> Option<&'a Expr> {
1541    if guard.descend() {
1542        return None;
1543    }
1544
1545    let mut parts = symbol.split('.');
1546    let head = parts.next()?;
1547    let mut expr = lookup_binding(scopes, head)?;
1548    let rest = parts.collect::<Vec<_>>();
1549    if rest.is_empty() {
1550        guard.ascend();
1551        return Some(expr);
1552    }
1553
1554    for segment in rest {
1555        expr = resolve_select(expr, &[segment.to_string()], scopes, guard)?;
1556    }
1557
1558    guard.ascend();
1559    Some(expr)
1560}
1561
1562fn resolve_select<'a>(
1563    target: &'a Expr,
1564    path: &[String],
1565    scopes: &[NixAttrEntriesRef<'a>],
1566    guard: &mut RecursionGuard<()>,
1567) -> Option<&'a Expr> {
1568    if guard.descend() {
1569        return None;
1570    }
1571
1572    let result = match target {
1573        Expr::AttrSet(entries) => find_attr(
1574            entries,
1575            &path.iter().map(String::as_str).collect::<Vec<_>>(),
1576            guard,
1577        ),
1578        Expr::Let { bindings, body } => {
1579            let scopes = extend_scopes(scopes, bindings);
1580            resolve_select(body, path, &scopes, guard)
1581        }
1582        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, guard)
1583            .and_then(|resolved| resolve_select(resolved, path, scopes, guard)),
1584        Expr::Select {
1585            target: inner_target,
1586            path: inner_path,
1587        } => resolve_select(inner_target, inner_path, scopes, guard)
1588            .and_then(|resolved| resolve_select(resolved, path, scopes, guard)),
1589        _ => None,
1590    };
1591    guard.ascend();
1592    result
1593}
1594
1595fn interpolate_string(value: &str, scopes: &[&[(Vec<String>, Expr)]]) -> String {
1596    let mut result = String::new();
1597    let mut index = 0usize;
1598
1599    while let Some(relative_start) = value[index..].find("${") {
1600        let start = index + relative_start;
1601        result.push_str(&value[index..start]);
1602
1603        let placeholder_start = start + 2;
1604        let Some(relative_end) = value[placeholder_start..].find('}') else {
1605            result.push_str(&value[start..]);
1606            return result;
1607        };
1608        let end = placeholder_start + relative_end;
1609        let placeholder = value[placeholder_start..end].trim();
1610        if !placeholder.is_empty()
1611            && placeholder
1612                .chars()
1613                .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
1614            && let Some(resolved) =
1615                resolve_symbol(placeholder, scopes, &mut RecursionGuard::depth_only())
1616            && let Some(replacement) =
1617                expr_as_string_with_scopes(resolved, scopes, &mut RecursionGuard::depth_only())
1618        {
1619            result.push_str(&replacement);
1620        } else {
1621            result.push_str(&value[start..=end]);
1622        }
1623
1624        index = end + 1;
1625    }
1626
1627    result.push_str(&value[index..]);
1628    result
1629}
1630
1631fn extract_default_nix_package(
1632    path: &Path,
1633    expr: &Expr,
1634    scopes: &[&[(Vec<String>, Expr)]],
1635    depth: usize,
1636) -> Result<PackageData, String> {
1637    if depth > 2 {
1638        return Err("default.nix exceeded supported local import depth".to_string());
1639    }
1640
1641    match expr {
1642        Expr::Let { bindings, body } => {
1643            let scopes = extend_scopes(scopes, bindings);
1644            extract_default_nix_package(path, body, &scopes, depth)
1645        }
1646        Expr::Application(parts) => {
1647            if let Some(derivation) = find_mk_derivation_attrset(expr) {
1648                return build_default_package_from_attrset(path, derivation, scopes);
1649            }
1650
1651            if let Some((imported_expr, imported_path)) =
1652                try_follow_local_nix_application(path, parts, scopes)
1653            {
1654                return extract_default_nix_package(
1655                    &imported_path,
1656                    &imported_expr,
1657                    &Vec::new(),
1658                    depth + 1,
1659                );
1660            }
1661
1662            if let Some(package) = parts
1663                .first()
1664                .and_then(|part| extract_flake_compat_package_from_expr(path, part, scopes, depth))
1665            {
1666                return Ok(package);
1667            }
1668
1669            Err("default.nix did not contain a supported mkDerivation call".to_string())
1670        }
1671        Expr::Select {
1672            target,
1673            path: select_path,
1674        } => {
1675            if let Some(package) =
1676                extract_flake_compat_package_from_select(path, target, select_path, scopes, depth)
1677            {
1678                return Ok(package);
1679            }
1680
1681            if let Some((imported_expr, imported_path)) =
1682                try_follow_selected_local_import(path, target, select_path, scopes)
1683            {
1684                return extract_default_nix_package(
1685                    &imported_path,
1686                    &imported_expr,
1687                    &Vec::new(),
1688                    depth + 1,
1689                );
1690            }
1691
1692            if let Some(resolved) = resolve_select(
1693                target,
1694                select_path,
1695                scopes,
1696                &mut RecursionGuard::depth_only(),
1697            ) {
1698                return extract_default_nix_package(path, resolved, scopes, depth);
1699            }
1700
1701            Err("default.nix did not contain a supported mkDerivation call".to_string())
1702        }
1703        Expr::Symbol(_) => extract_flake_compat_package_from_expr(path, expr, scopes, depth)
1704            .ok_or_else(|| "default.nix did not contain a supported mkDerivation call".to_string()),
1705        _ => Err("default.nix did not contain a supported mkDerivation call".to_string()),
1706    }
1707}
1708
1709fn build_default_package_from_attrset(
1710    path: &Path,
1711    derivation: &[(Vec<String>, Expr)],
1712    scopes: &[&[(Vec<String>, Expr)]],
1713) -> Result<PackageData, String> {
1714    let mut package = default_default_nix_package_data();
1715    package.name = find_string_attr_with_scopes(derivation, &["pname"], scopes).or_else(|| {
1716        find_string_attr_with_scopes(derivation, &["name"], scopes)
1717            .map(|name| split_derivation_name(&name).0)
1718    });
1719    package.version =
1720        find_string_attr_with_scopes(derivation, &["version"], scopes).or_else(|| {
1721            find_string_attr_with_scopes(derivation, &["name"], scopes)
1722                .and_then(|name| split_derivation_name(&name).1)
1723        });
1724    package.description =
1725        find_string_attr_with_scopes(derivation, &["meta", "description"], scopes)
1726            .or_else(|| find_string_attr_with_scopes(derivation, &["description"], scopes));
1727    package.homepage_url = find_string_attr_with_scopes(derivation, &["meta", "homepage"], scopes)
1728        .or_else(|| find_string_attr_with_scopes(derivation, &["homepage"], scopes));
1729    package.extracted_license_statement = find_attr(
1730        derivation,
1731        &["meta", "license"],
1732        &mut RecursionGuard::depth_only(),
1733    )
1734    .and_then(|expr| {
1735        expr_to_scalar_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1736    })
1737    .or_else(|| {
1738        find_attr(derivation, &["license"], &mut RecursionGuard::depth_only()).and_then(|expr| {
1739            expr_to_scalar_string_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1740        })
1741    });
1742    package.dependencies = [
1743        build_list_dependencies(derivation, "nativeBuildInputs", false, scopes),
1744        build_list_dependencies(derivation, "buildInputs", true, scopes),
1745        build_list_dependencies(derivation, "propagatedBuildInputs", true, scopes),
1746        build_list_dependencies(derivation, "checkInputs", false, scopes),
1747    ]
1748    .concat();
1749    if package.name.is_none() {
1750        package.name = fallback_name(path).map(truncate_field);
1751    }
1752    package.purl = package
1753        .name
1754        .as_deref()
1755        .and_then(|name| build_nix_purl(name, package.version.as_deref()));
1756
1757    Ok(package)
1758}
1759
1760fn try_follow_local_nix_application(
1761    path: &Path,
1762    parts: &[Expr],
1763    scopes: &[&[(Vec<String>, Expr)]],
1764) -> Option<(Expr, std::path::PathBuf)> {
1765    let head = parts.first().and_then(expr_as_symbol)?;
1766    let is_supported_wrapper = head == "import" || head.ends_with("callPackage");
1767    if !is_supported_wrapper {
1768        return None;
1769    }
1770
1771    let local_path = parts.get(1).and_then(|expr| {
1772        expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1773    })?;
1774    if !is_local_nix_path(&local_path) {
1775        return None;
1776    }
1777
1778    let resolved_path = resolve_local_nix_path(path, &local_path)?;
1779    let content = read_file_to_string(&resolved_path, None).ok()?;
1780    let expr = parse_nix_expr(&content).ok()?;
1781    Some((expr, resolved_path))
1782}
1783
1784fn try_follow_selected_local_import(
1785    path: &Path,
1786    target: &Expr,
1787    select_path: &[String],
1788    scopes: &[&[(Vec<String>, Expr)]],
1789) -> Option<(Expr, std::path::PathBuf)> {
1790    let Expr::Application(parts) = target else {
1791        return None;
1792    };
1793
1794    let (imported_expr, imported_path) = try_follow_local_nix_application(path, parts, scopes)?;
1795    let selected = attrset_entries(&imported_expr).and_then(|entries| {
1796        find_attr(
1797            entries,
1798            &select_path.iter().map(String::as_str).collect::<Vec<_>>(),
1799            &mut RecursionGuard::depth_only(),
1800        )
1801    })?;
1802    Some((selected.clone(), imported_path))
1803}
1804
1805fn extract_flake_compat_package_from_expr(
1806    path: &Path,
1807    expr: &Expr,
1808    scopes: &[&[(Vec<String>, Expr)]],
1809    depth: usize,
1810) -> Option<PackageData> {
1811    if depth > 2 {
1812        return None;
1813    }
1814
1815    match expr {
1816        Expr::Select {
1817            target,
1818            path: select_path,
1819        } => extract_flake_compat_package_from_select(path, target, select_path, scopes, depth),
1820        Expr::Let { bindings, body } => {
1821            let scopes = extend_scopes(scopes, bindings);
1822            extract_flake_compat_package_from_expr(path, body, &scopes, depth)
1823        }
1824        Expr::Symbol(symbol) => {
1825            if let Some((head, rest)) = symbol.split_once('.') {
1826                let select_path = rest.split('.').map(ToOwned::to_owned).collect::<Vec<_>>();
1827                resolve_symbol(head, scopes, &mut RecursionGuard::depth_only())
1828                    .and_then(|resolved| {
1829                        extract_flake_compat_package_from_select(
1830                            path,
1831                            resolved,
1832                            &select_path,
1833                            scopes,
1834                            depth,
1835                        )
1836                    })
1837                    .or_else(|| {
1838                        let target = Expr::Symbol(head.to_string());
1839                        extract_flake_compat_package_from_select(
1840                            path,
1841                            &target,
1842                            &select_path,
1843                            scopes,
1844                            depth,
1845                        )
1846                    })
1847                    .or_else(|| {
1848                        resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only()).and_then(
1849                            |resolved| {
1850                                extract_flake_compat_package_from_expr(
1851                                    path, resolved, scopes, depth,
1852                                )
1853                            },
1854                        )
1855                    })
1856            } else {
1857                resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only()).and_then(
1858                    |resolved| {
1859                        extract_flake_compat_package_from_expr(path, resolved, scopes, depth)
1860                    },
1861                )
1862            }
1863        }
1864        _ => None,
1865    }
1866}
1867
1868fn extract_flake_compat_package_from_select(
1869    path: &Path,
1870    target: &Expr,
1871    select_path: &[String],
1872    scopes: &[&[(Vec<String>, Expr)]],
1873    depth: usize,
1874) -> Option<PackageData> {
1875    if depth > 2 || select_path.first().map(String::as_str) != Some("defaultNix") {
1876        return None;
1877    }
1878
1879    let source_root = resolve_flake_compat_source_root(path, target, scopes, 0)?;
1880    let mut package = default_default_nix_package_data();
1881    package.name = source_root
1882        .file_name()
1883        .and_then(|name| name.to_str())
1884        .map(ToOwned::to_owned)
1885        .map(truncate_field)
1886        .or_else(|| fallback_name(path));
1887    package.purl = package
1888        .name
1889        .as_deref()
1890        .and_then(|name| build_nix_purl(name, None));
1891    mark_flake_compat_wrapper(&mut package);
1892    Some(package)
1893}
1894
1895fn resolve_flake_compat_source_root(
1896    path: &Path,
1897    target: &Expr,
1898    scopes: &[&[(Vec<String>, Expr)]],
1899    depth: usize,
1900) -> Option<std::path::PathBuf> {
1901    if depth > 8 {
1902        return None;
1903    }
1904
1905    match target {
1906        Expr::Application(parts) => source_root_from_flake_compat_application(path, parts, scopes),
1907        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, &mut RecursionGuard::depth_only())
1908            .and_then(|resolved| {
1909                resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)
1910            }),
1911        Expr::Let { bindings, body } => {
1912            let scopes = extend_scopes(scopes, bindings);
1913            resolve_flake_compat_source_root(path, body, &scopes, depth + 1)
1914        }
1915        Expr::Select {
1916            target: inner_target,
1917            path: inner_path,
1918        } => resolve_select(
1919            inner_target,
1920            inner_path,
1921            scopes,
1922            &mut RecursionGuard::depth_only(),
1923        )
1924        .and_then(|resolved| resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)),
1925        _ => None,
1926    }
1927}
1928
1929fn source_root_from_flake_compat_application(
1930    path: &Path,
1931    parts: &[Expr],
1932    scopes: &[&[(Vec<String>, Expr)]],
1933) -> Option<std::path::PathBuf> {
1934    let head = parts.first().and_then(expr_as_symbol)?;
1935    if head != "import" {
1936        return None;
1937    }
1938
1939    let import_path = parts.get(1).and_then(|expr| {
1940        expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1941    })?;
1942    if !is_local_nix_path(&import_path) {
1943        return None;
1944    }
1945
1946    let args = parts.iter().find_map(attrset_entries)?;
1947    let src_value =
1948        find_attr(args, &["src"], &mut RecursionGuard::depth_only()).and_then(|expr| {
1949            expr_as_symbol_with_scopes(expr, scopes, &mut RecursionGuard::depth_only())
1950        })?;
1951    if !is_local_path(&src_value) {
1952        return None;
1953    }
1954
1955    resolve_local_path(path, &src_value)
1956}
1957
1958fn is_local_path(value: &str) -> bool {
1959    value.starts_with("./") || value.starts_with("../")
1960}
1961
1962fn is_local_nix_path(value: &str) -> bool {
1963    is_local_path(value) && value.ends_with(".nix")
1964}
1965
1966fn resolve_local_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1967    let base = path.parent()?;
1968    let resolved = base.join(value);
1969    resolved.exists().then_some(resolved)
1970}
1971
1972fn resolve_local_nix_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1973    resolve_local_path(path, value).filter(|resolved| resolved.is_file())
1974}
1975
1976fn extract_flake_compat_default_package_from_content(
1977    path: &Path,
1978    content: &str,
1979) -> Result<PackageData, String> {
1980    if !content.contains("defaultNix") || !content.contains("flake-compat.nix") {
1981        return Err("default.nix did not contain a supported mkDerivation call".to_string());
1982    }
1983
1984    let src_value = extract_local_flake_compat_src_value(content).unwrap_or("./.".to_string());
1985    let mut package = default_default_nix_package_data();
1986    package.name = normalize_local_source_root(path, &src_value)
1987        .and_then(|source_root| {
1988            source_root
1989                .file_name()
1990                .and_then(|name| name.to_str())
1991                .filter(|name| *name != ".")
1992                .map(ToOwned::to_owned)
1993        })
1994        .map(truncate_field)
1995        .or_else(|| fallback_name(path));
1996    if package.name.is_none() {
1997        return Err("default.nix did not contain a supported mkDerivation call".to_string());
1998    }
1999    package.purl = package
2000        .name
2001        .as_deref()
2002        .and_then(|name| build_nix_purl(name, None));
2003    mark_flake_compat_wrapper(&mut package);
2004    Ok(package)
2005}
2006
2007fn mark_flake_compat_wrapper(package: &mut PackageData) {
2008    let mut extra_data = package.extra_data.clone().unwrap_or_default();
2009    extra_data.insert(
2010        "nix_wrapper_kind".to_string(),
2011        JsonValue::String("flake_compat".to_string()),
2012    );
2013    package.extra_data = Some(extra_data);
2014}
2015
2016fn extract_local_flake_compat_src_value(content: &str) -> Option<String> {
2017    let src_index = content.find("src")?;
2018    let after_src = &content[src_index + 3..];
2019    let equals_index = after_src.find('=')?;
2020    let remainder = after_src[equals_index + 1..].trim_start();
2021    let end_index = remainder.find([';', '}', '\n']).unwrap_or(remainder.len());
2022    let candidate = remainder[..end_index].trim();
2023    if is_local_path(candidate) {
2024        Some(candidate.to_string())
2025    } else {
2026        None
2027    }
2028}
2029
2030fn normalize_local_source_root(path: &Path, value: &str) -> Option<std::path::PathBuf> {
2031    match value {
2032        "." | "./." => path.parent().map(|parent| parent.to_path_buf()),
2033        _ if value.ends_with("/.") => resolve_local_path(path, value.trim_end_matches("/.")),
2034        _ => resolve_local_path(path, value),
2035    }
2036}
2037
2038fn split_derivation_name(name: &str) -> (String, Option<String>) {
2039    let mut parts = name.rsplitn(2, '-');
2040    let maybe_version = parts
2041        .next()
2042        .filter(|value| value.chars().any(|ch| ch.is_ascii_digit()));
2043    let maybe_name = parts.next();
2044
2045    match (maybe_name, maybe_version) {
2046        (Some(package_name), Some(version)) => {
2047            (package_name.to_string(), Some(version.to_string()))
2048        }
2049        _ => (name.to_string(), None),
2050    }
2051}
2052
2053fn default_flake_package_data() -> PackageData {
2054    PackageData {
2055        package_type: Some(PackageType::Nix),
2056        primary_language: Some("Nix".to_string()),
2057        datasource_id: Some(DatasourceId::NixFlakeNix),
2058        ..Default::default()
2059    }
2060}
2061
2062fn default_flake_lock_package_data() -> PackageData {
2063    PackageData {
2064        package_type: Some(PackageType::Nix),
2065        primary_language: Some("JSON".to_string()),
2066        datasource_id: Some(DatasourceId::NixFlakeLock),
2067        ..Default::default()
2068    }
2069}
2070
2071fn default_default_nix_package_data() -> PackageData {
2072    PackageData {
2073        package_type: Some(PackageType::Nix),
2074        primary_language: Some("Nix".to_string()),
2075        datasource_id: Some(DatasourceId::NixDefaultNix),
2076        ..Default::default()
2077    }
2078}