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