Skip to main content

provenant/parsers/
nix.rs

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