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::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
10
11use super::PackageParser;
12
13const MAX_RECURSION_DEPTH: usize = 50;
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    depth: usize,
443}
444
445impl Parser {
446    fn new(tokens: Vec<Token>) -> Self {
447        Self {
448            tokens,
449            index: 0,
450            depth: 0,
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.depth > MAX_RECURSION_DEPTH {
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            self.depth += 1;
471            let result = self.parse_expr();
472            self.depth -= 1;
473            return result;
474        }
475
476        if self.looks_like_prefixed_lambda_binder_set()? {
477            self.index += 1;
478            self.skip_lambda_binder_set()?;
479            self.expect(&Token::Colon)?;
480            self.depth += 1;
481            let result = self.parse_expr();
482            self.depth -= 1;
483            return result;
484        }
485
486        let first = self.parse_term()?;
487        if self.consume(&Token::Colon) {
488            self.depth += 1;
489            let result = self.parse_expr();
490            self.depth -= 1;
491            return result;
492        }
493
494        let mut terms = vec![first];
495        while self.can_start_term() {
496            terms.push(self.parse_term()?);
497        }
498
499        let expr = if terms.len() == 1 {
500            terms.pop().unwrap()
501        } else {
502            Expr::Application(terms)
503        };
504
505        self.parse_postfix(expr)
506    }
507
508    fn parse_postfix(&mut self, mut expr: Expr) -> Result<Expr, String> {
509        while self.consume(&Token::Dot) {
510            let mut path = vec![self.take_attr_key()?];
511            while self.consume(&Token::Dot) {
512                path.push(self.take_attr_key()?);
513            }
514            expr = Expr::Select {
515                target: Box::new(expr),
516                path,
517            };
518        }
519
520        Ok(expr)
521    }
522
523    fn parse_term(&mut self) -> Result<Expr, String> {
524        match self.peek() {
525            Some(Token::Ident(keyword)) if keyword == "let" => self.parse_let_in_expr(),
526            Some(Token::Ident(keyword)) if keyword == "with" => {
527                self.index += 1;
528                self.depth += 1;
529                let _ = self.parse_expr()?;
530                self.depth -= 1;
531                self.expect(&Token::Semicolon)?;
532                self.depth += 1;
533                let result = self.parse_expr();
534                self.depth -= 1;
535                result
536            }
537            Some(Token::Ident(keyword)) if keyword == "rec" => {
538                if matches!(self.peek_n(1), Some(Token::LBrace)) {
539                    self.index += 1;
540                    self.parse_attrset()
541                } else {
542                    self.parse_symbol()
543                }
544            }
545            Some(Token::LBrace) => self.parse_attrset(),
546            Some(Token::LBracket) => self.parse_list(),
547            Some(Token::LParen) => {
548                self.index += 1;
549                self.depth += 1;
550                let expr = self.parse_expr()?;
551                self.depth -= 1;
552                self.expect(&Token::RParen)?;
553                Ok(expr)
554            }
555            Some(Token::String(_)) => self.parse_string(),
556            Some(Token::Ident(_)) => self.parse_symbol(),
557            _ => Err("expected expression".to_string()),
558        }
559    }
560
561    fn parse_let_in_expr(&mut self) -> Result<Expr, String> {
562        self.take_exact_ident("let")?;
563        let mut bindings = Vec::new();
564
565        while !matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "in") {
566            if self.peek().is_none() {
567                return Err("unterminated let expression".to_string());
568            }
569
570            if bindings.len() >= MAX_ITERATION_COUNT {
571                warn!("parse_let_in_expr exceeded MAX_ITERATION_COUNT bindings limit");
572                break;
573            }
574
575            if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
576                bindings.extend(self.parse_inherit_entries()?);
577                continue;
578            }
579
580            let key = self.parse_attr_path()?;
581            self.expect(&Token::Equals)?;
582            self.depth += 1;
583            let value = self.parse_expr()?;
584            self.depth -= 1;
585            self.expect(&Token::Semicolon)?;
586            bindings.push((key, value));
587        }
588
589        self.take_exact_ident("in")?;
590        self.depth += 1;
591        let body = self.parse_expr()?;
592        self.depth -= 1;
593        Ok(Expr::Let {
594            bindings,
595            body: Box::new(body),
596        })
597    }
598
599    fn parse_attrset(&mut self) -> Result<Expr, String> {
600        self.expect(&Token::LBrace)?;
601        let mut entries = Vec::new();
602
603        loop {
604            if self.consume(&Token::RBrace) {
605                return Ok(Expr::AttrSet(entries));
606            }
607
608            if self.peek().is_none() {
609                return Err("unterminated attribute set".to_string());
610            }
611
612            if entries.len() >= MAX_ITERATION_COUNT {
613                warn!("parse_attrset exceeded MAX_ITERATION_COUNT entries limit");
614                break;
615            }
616
617            if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
618                entries.extend(self.parse_inherit_entries()?);
619                continue;
620            }
621
622            let key = self.parse_attr_path()?;
623            self.expect(&Token::Equals)?;
624            self.depth += 1;
625            let value = self.parse_expr()?;
626            self.depth -= 1;
627            self.expect(&Token::Semicolon)?;
628            entries.push((key, value));
629        }
630
631        Ok(Expr::AttrSet(entries))
632    }
633
634    fn parse_attr_path(&mut self) -> Result<Vec<String>, String> {
635        let mut path = vec![self.take_attr_key()?];
636        while self.consume(&Token::Dot) {
637            path.push(self.take_attr_key()?);
638        }
639        Ok(path)
640    }
641
642    fn parse_inherit_entries(&mut self) -> Result<Vec<(Vec<String>, Expr)>, String> {
643        self.take_exact_ident("inherit")?;
644
645        let inherit_from = if self.consume(&Token::LParen) {
646            self.depth += 1;
647            let expr = self.parse_expr()?;
648            self.depth -= 1;
649            self.expect(&Token::RParen)?;
650            Some(expr)
651        } else {
652            None
653        };
654
655        let mut entries = Vec::new();
656        while !self.consume(&Token::Semicolon) {
657            if self.peek().is_none() {
658                return Err("unterminated inherit statement".to_string());
659            }
660
661            if entries.len() >= MAX_ITERATION_COUNT {
662                warn!("parse_inherit_entries exceeded MAX_ITERATION_COUNT entries limit");
663                break;
664            }
665
666            let name = self.take_attr_key()?;
667            let value = match &inherit_from {
668                Some(source) => Expr::Select {
669                    target: Box::new(source.clone()),
670                    path: vec![name.clone()],
671                },
672                None => Expr::Symbol(name.clone()),
673            };
674            entries.push((vec![name], value));
675        }
676
677        Ok(entries)
678    }
679
680    fn parse_list(&mut self) -> Result<Expr, String> {
681        self.expect(&Token::LBracket)?;
682        let mut items = Vec::new();
683        while !self.consume(&Token::RBracket) {
684            if self.peek().is_none() {
685                return Err("unterminated list".to_string());
686            }
687
688            if items.len() >= MAX_ITERATION_COUNT {
689                warn!("parse_list exceeded MAX_ITERATION_COUNT items limit");
690                break;
691            }
692
693            self.depth += 1;
694            items.push(self.parse_expr()?);
695            self.depth -= 1;
696        }
697        Ok(Expr::List(items))
698    }
699
700    fn parse_string(&mut self) -> Result<Expr, String> {
701        match self.next() {
702            Some(Token::String(value)) => Ok(Expr::String(value)),
703            _ => Err("expected string".to_string()),
704        }
705    }
706
707    fn parse_symbol(&mut self) -> Result<Expr, String> {
708        let mut parts = vec![self.take_ident()?];
709        while self.consume(&Token::Dot) {
710            parts.push(self.take_ident()?);
711        }
712        Ok(Expr::Symbol(parts.join(".")))
713    }
714
715    fn take_ident(&mut self) -> Result<String, String> {
716        match self.next() {
717            Some(Token::Ident(value)) => Ok(value),
718            _ => Err("expected identifier".to_string()),
719        }
720    }
721
722    fn take_exact_ident(&mut self, expected: &str) -> Result<(), String> {
723        match self.next() {
724            Some(Token::Ident(value)) if value == expected => Ok(()),
725            _ => Err(format!("expected {expected}")),
726        }
727    }
728
729    fn take_attr_key(&mut self) -> Result<String, String> {
730        match self.next() {
731            Some(Token::Ident(value)) | Some(Token::String(value)) => Ok(value),
732            _ => Err("expected attribute key".to_string()),
733        }
734    }
735
736    fn can_start_term(&self) -> bool {
737        matches!(
738            self.peek(),
739            Some(Token::LBrace)
740                | Some(Token::LBracket)
741                | Some(Token::LParen)
742                | Some(Token::String(_))
743                | Some(Token::Ident(_))
744        )
745    }
746
747    fn looks_like_lambda_binder_set(&self) -> Result<bool, String> {
748        if self.peek() != Some(&Token::LBrace) {
749            return Ok(false);
750        }
751
752        self.looks_like_lambda_binder_set_from(self.index)
753    }
754
755    fn looks_like_prefixed_lambda_binder_set(&self) -> Result<bool, String> {
756        match (self.peek(), self.peek_n(1)) {
757            (Some(Token::Ident(prefix)), Some(Token::LBrace)) if prefix.ends_with('@') => {
758                self.looks_like_lambda_binder_set_from(self.index + 1)
759            }
760            _ => Ok(false),
761        }
762    }
763
764    fn looks_like_lambda_binder_set_from(&self, start_index: usize) -> Result<bool, String> {
765        if self.tokens.get(start_index) != Some(&Token::LBrace) {
766            return Ok(false);
767        }
768
769        let mut depth = 0usize;
770        let mut index = start_index;
771
772        while let Some(token) = self.tokens.get(index) {
773            match token {
774                Token::LBrace => depth += 1,
775                Token::RBrace => {
776                    depth = depth.saturating_sub(1);
777                    if depth == 0 {
778                        return Ok(matches!(self.tokens.get(index + 1), Some(Token::Colon)));
779                    }
780                }
781                Token::Equals | Token::Semicolon if depth == 1 => return Ok(false),
782                _ => {}
783            }
784
785            index += 1;
786        }
787
788        Err("unterminated lambda binder set".to_string())
789    }
790
791    fn skip_lambda_binder_set(&mut self) -> Result<(), String> {
792        self.expect(&Token::LBrace)?;
793        let mut depth = 1usize;
794
795        while depth > 0 {
796            match self.next() {
797                Some(Token::LBrace) => depth += 1,
798                Some(Token::RBrace) => depth = depth.saturating_sub(1),
799                Some(_) => {}
800                None => return Err("unterminated lambda binder set".to_string()),
801            }
802        }
803
804        Ok(())
805    }
806
807    fn expect(&mut self, expected: &Token) -> Result<(), String> {
808        if self.consume(expected) {
809            Ok(())
810        } else {
811            Err(format!("expected {:?}", expected))
812        }
813    }
814
815    fn consume(&mut self, expected: &Token) -> bool {
816        if self.peek() == Some(expected) {
817            self.index += 1;
818            true
819        } else {
820            false
821        }
822    }
823
824    fn peek(&self) -> Option<&Token> {
825        self.tokens.get(self.index)
826    }
827
828    fn peek_n(&self, offset: usize) -> Option<&Token> {
829        self.tokens.get(self.index + offset)
830    }
831
832    fn next(&mut self) -> Option<Token> {
833        let token = self.tokens.get(self.index).cloned();
834        if token.is_some() {
835            self.index += 1;
836        }
837        token
838    }
839}
840
841fn parse_flake_nix(path: &Path, content: &str) -> Result<PackageData, String> {
842    let expr = parse_nix_expr(content)?;
843    let scopes = Vec::new();
844    let (root, scopes) = root_attrset_with_scopes(&expr, &scopes, 0)
845        .ok_or_else(|| "flake.nix root was not an attribute set".to_string())?;
846
847    let mut package = default_flake_package_data();
848    package.name = fallback_name(path).map(truncate_field);
849    package.description =
850        find_string_attr_with_scopes(root, &["description"], &scopes).map(truncate_field);
851    package.purl = package
852        .name
853        .as_deref()
854        .and_then(|name| build_nix_purl(name, None));
855    package.dependencies = build_flake_dependencies(root, &scopes);
856
857    Ok(package)
858}
859
860fn parse_default_nix(path: &Path, content: &str) -> Result<PackageData, String> {
861    match parse_nix_expr(content) {
862        Ok(expr) => extract_default_nix_package(path, &expr, &Vec::new(), 0)
863            .or_else(|_| extract_flake_compat_default_package_from_content(path, content)),
864        Err(parse_error) => extract_flake_compat_default_package_from_content(path, content)
865            .map_err(|_| parse_error),
866    }
867}
868
869fn parse_flake_lock(path: &Path, json: &JsonValue) -> Result<PackageData, String> {
870    let version = json
871        .get("version")
872        .and_then(JsonValue::as_i64)
873        .ok_or_else(|| "flake.lock missing integer version".to_string())?;
874    let root = json
875        .get("root")
876        .and_then(JsonValue::as_str)
877        .ok_or_else(|| "flake.lock missing root".to_string())?;
878    let nodes = json
879        .get("nodes")
880        .and_then(JsonValue::as_object)
881        .ok_or_else(|| "flake.lock missing nodes".to_string())?;
882    let root_node = nodes
883        .get(root)
884        .and_then(JsonValue::as_object)
885        .ok_or_else(|| "flake.lock root node missing".to_string())?;
886    let root_inputs = root_node
887        .get("inputs")
888        .and_then(JsonValue::as_object)
889        .ok_or_else(|| "flake.lock root node missing inputs".to_string())?;
890
891    let mut package = default_flake_lock_package_data();
892    package.name = fallback_name(path).map(truncate_field);
893    package.purl = package
894        .name
895        .as_deref()
896        .and_then(|name| build_nix_purl(name, None));
897
898    let mut extra_data = HashMap::new();
899    extra_data.insert("lock_version".to_string(), JsonValue::from(version));
900    extra_data.insert("root".to_string(), JsonValue::String(root.to_string()));
901    package.extra_data = Some(extra_data);
902
903    package.dependencies = root_inputs
904        .iter()
905        .take(MAX_ITERATION_COUNT)
906        .filter_map(|(input_name, node_ref)| build_lock_dependency(input_name, node_ref, nodes))
907        .collect();
908    package
909        .dependencies
910        .sort_by(|left, right| left.purl.cmp(&right.purl));
911
912    Ok(package)
913}
914
915fn build_lock_dependency(
916    input_name: &str,
917    node_ref: &JsonValue,
918    nodes: &serde_json::Map<String, JsonValue>,
919) -> Option<Dependency> {
920    let node_id = node_ref.as_str()?;
921    let node = nodes.get(node_id)?.as_object()?;
922    let locked = node.get("locked").and_then(JsonValue::as_object)?;
923    let revision = locked.get("rev").and_then(JsonValue::as_str);
924
925    let mut extra_data = HashMap::new();
926    for key in [
927        "type",
928        "owner",
929        "repo",
930        "narHash",
931        "lastModified",
932        "revCount",
933        "url",
934        "path",
935        "dir",
936        "host",
937    ] {
938        if let Some(value) = locked.get(key) {
939            extra_data.insert(normalize_extra_key(key), value.clone());
940        }
941    }
942    if let Some(value) = node.get("flake").and_then(JsonValue::as_bool) {
943        extra_data.insert("flake".to_string(), JsonValue::Bool(value));
944    }
945    if let Some(original) = node.get("original").and_then(JsonValue::as_object) {
946        if let Some(value) = original.get("type") {
947            extra_data.insert("original_type".to_string(), value.clone());
948        }
949        if let Some(value) = original.get("id") {
950            extra_data.insert("original_id".to_string(), value.clone());
951        }
952        if let Some(value) = original.get("ref") {
953            extra_data.insert("original_ref".to_string(), value.clone());
954        }
955    }
956
957    Some(Dependency {
958        purl: build_nix_purl(input_name, revision),
959        extracted_requirement: build_locked_requirement(locked, node.get("original"))
960            .map(truncate_field),
961        scope: Some("inputs".to_string()),
962        is_runtime: Some(false),
963        is_optional: Some(false),
964        is_pinned: Some(revision.is_some()),
965        is_direct: Some(true),
966        resolved_package: None,
967        extra_data: (!extra_data.is_empty()).then_some(extra_data),
968    })
969}
970
971fn build_locked_requirement(
972    locked: &serde_json::Map<String, JsonValue>,
973    original: Option<&JsonValue>,
974) -> Option<String> {
975    let source_type = locked.get("type").and_then(JsonValue::as_str).or_else(|| {
976        original
977            .and_then(|value| value.get("type"))
978            .and_then(JsonValue::as_str)
979    });
980
981    match source_type {
982        Some("github") => {
983            let owner = locked.get("owner").and_then(JsonValue::as_str)?;
984            let repo = locked.get("repo").and_then(JsonValue::as_str)?;
985            Some(format!("github:{owner}/{repo}"))
986        }
987        Some("indirect") => original
988            .and_then(|value| value.get("id"))
989            .and_then(JsonValue::as_str)
990            .map(ToOwned::to_owned),
991        _ => locked
992            .get("url")
993            .and_then(JsonValue::as_str)
994            .map(ToOwned::to_owned),
995    }
996}
997
998fn normalize_extra_key(key: &str) -> String {
999    match key {
1000        "type" => "source_type".to_string(),
1001        "narHash" => "nar_hash".to_string(),
1002        "lastModified" => "last_modified".to_string(),
1003        "revCount" => "rev_count".to_string(),
1004        other => other.to_string(),
1005    }
1006}
1007
1008fn build_flake_dependencies(
1009    root: &[(Vec<String>, Expr)],
1010    scopes: &[&[(Vec<String>, Expr)]],
1011) -> Vec<Dependency> {
1012    let mut inputs: HashMap<String, FlakeInputInfo> = HashMap::new();
1013
1014    for (path, expr) in root {
1015        if path.first().map(String::as_str) != Some("inputs") {
1016            continue;
1017        }
1018
1019        if path.len() == 1 {
1020            if let Some(entries) = attrset_entries(expr) {
1021                collect_input_entries(entries, scopes, &mut inputs, None);
1022            }
1023            continue;
1024        }
1025
1026        collect_input_path(&path[1..], expr, scopes, &mut inputs);
1027    }
1028
1029    let mut dependencies = inputs
1030        .into_iter()
1031        .map(|(name, info)| {
1032            let mut extra_data = HashMap::new();
1033            if info.follows.len() == 1 {
1034                extra_data.insert(
1035                    "follows".to_string(),
1036                    JsonValue::String(info.follows[0].clone()),
1037                );
1038            } else if !info.follows.is_empty() {
1039                extra_data.insert(
1040                    "follows".to_string(),
1041                    JsonValue::Array(
1042                        info.follows
1043                            .iter()
1044                            .cloned()
1045                            .map(JsonValue::String)
1046                            .collect(),
1047                    ),
1048                );
1049            }
1050            if let Some(flake) = info.flake {
1051                extra_data.insert("flake".to_string(), JsonValue::Bool(flake));
1052            }
1053
1054            Dependency {
1055                purl: build_nix_purl(&name, None),
1056                extracted_requirement: info.requirement.map(truncate_field),
1057                scope: Some("inputs".to_string()),
1058                is_runtime: Some(false),
1059                is_optional: Some(false),
1060                is_pinned: Some(false),
1061                is_direct: Some(true),
1062                resolved_package: None,
1063                extra_data: (!extra_data.is_empty()).then_some(extra_data),
1064            }
1065        })
1066        .collect::<Vec<_>>();
1067
1068    dependencies.sort_by(|left, right| left.purl.cmp(&right.purl));
1069    dependencies
1070}
1071
1072fn collect_input_entries(
1073    entries: &[(Vec<String>, Expr)],
1074    scopes: &[&[(Vec<String>, Expr)]],
1075    inputs: &mut HashMap<String, FlakeInputInfo>,
1076    current_input: Option<&str>,
1077) {
1078    for (path, expr) in entries {
1079        if let Some(input_name) = current_input {
1080            apply_input_field(
1081                inputs.entry(input_name.to_string()).or_default(),
1082                path,
1083                expr,
1084                scopes,
1085            );
1086            continue;
1087        }
1088
1089        collect_input_path(path, expr, scopes, inputs);
1090    }
1091}
1092
1093fn collect_input_path(
1094    path: &[String],
1095    expr: &Expr,
1096    scopes: &[&[(Vec<String>, Expr)]],
1097    inputs: &mut HashMap<String, FlakeInputInfo>,
1098) {
1099    let Some(input_name) = path.first() else {
1100        return;
1101    };
1102
1103    if path.len() == 1 {
1104        match expr {
1105            Expr::AttrSet(entries) => {
1106                collect_input_entries(entries, scopes, inputs, Some(input_name))
1107            }
1108            Expr::String(value) => {
1109                inputs.entry(input_name.clone()).or_default().requirement = Some(value.clone())
1110            }
1111            Expr::Symbol(value) => {
1112                inputs.entry(input_name.clone()).or_default().requirement =
1113                    expr_as_string_with_scopes(&Expr::Symbol(value.clone()), scopes, 0)
1114            }
1115            _ => {}
1116        }
1117        return;
1118    }
1119
1120    apply_input_field(
1121        inputs.entry(input_name.clone()).or_default(),
1122        &path[1..],
1123        expr,
1124        scopes,
1125    );
1126}
1127
1128fn apply_input_field(
1129    info: &mut FlakeInputInfo,
1130    path: &[String],
1131    expr: &Expr,
1132    scopes: &[&[(Vec<String>, Expr)]],
1133) {
1134    if path == ["url"] {
1135        info.requirement = expr_as_string_with_scopes(expr, scopes, 0);
1136        return;
1137    }
1138
1139    if path == ["flake"] {
1140        info.flake = expr_as_bool_with_scopes(expr, scopes, 0);
1141        return;
1142    }
1143
1144    if path.len() == 3
1145        && path[0] == "inputs"
1146        && path[2] == "follows"
1147        && let Some(value) = expr_as_string_with_scopes(expr, scopes, 0)
1148    {
1149        info.follows.push(value);
1150    }
1151}
1152
1153fn build_list_dependencies(
1154    entries: &[(Vec<String>, Expr)],
1155    field_name: &str,
1156    runtime: bool,
1157    scopes: &[&[(Vec<String>, Expr)]],
1158) -> Vec<Dependency> {
1159    let Some(expr) = find_attr(entries, &[field_name], 0) else {
1160        return Vec::new();
1161    };
1162    let Some(items) = list_items_with_scopes(expr, scopes, 0) else {
1163        return Vec::new();
1164    };
1165
1166    items
1167        .iter()
1168        .take(MAX_ITERATION_COUNT)
1169        .flat_map(|expr| expr_to_dependency_symbols_with_scopes(expr, scopes, 0))
1170        .filter_map(|symbol| {
1171            let name = symbol.rsplit('.').next()?.to_string();
1172            Some(Dependency {
1173                purl: build_nix_purl(&name, None),
1174                extracted_requirement: None,
1175                scope: Some(field_name.to_string()),
1176                is_runtime: Some(runtime),
1177                is_optional: Some(false),
1178                is_pinned: Some(false),
1179                is_direct: Some(true),
1180                resolved_package: None,
1181                extra_data: None,
1182            })
1183        })
1184        .collect()
1185}
1186
1187fn expr_to_dependency_symbols_with_scopes(
1188    expr: &Expr,
1189    scopes: &[&[(Vec<String>, Expr)]],
1190    depth: usize,
1191) -> Vec<String> {
1192    if depth > MAX_RECURSION_DEPTH {
1193        warn!("expr_to_dependency_symbols_with_scopes exceeded MAX_RECURSION_DEPTH");
1194        return Vec::new();
1195    }
1196
1197    match expr {
1198        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, 0)
1199            .map(|resolved| expr_to_dependency_symbols_with_scopes(resolved, scopes, depth + 1))
1200            .unwrap_or_else(|| vec![symbol.clone()]),
1201        Expr::Application(parts) => parts
1202            .iter()
1203            .filter_map(|expr| expr_as_symbol_with_scopes(expr, scopes, 0))
1204            .collect(),
1205        Expr::Let { bindings, body } => {
1206            let scopes = extend_scopes(scopes, bindings);
1207            expr_to_dependency_symbols_with_scopes(body, &scopes, depth + 1)
1208        }
1209        Expr::Select { .. } => expr_as_symbol_with_scopes(expr, scopes, 0)
1210            .into_iter()
1211            .collect(),
1212        _ => Vec::new(),
1213    }
1214}
1215
1216fn fallback_name(path: &Path) -> Option<String> {
1217    path.parent()
1218        .and_then(|parent| parent.file_name())
1219        .and_then(|name| name.to_str())
1220        .map(ToOwned::to_owned)
1221}
1222
1223fn build_nix_purl(name: &str, version: Option<&str>) -> Option<String> {
1224    let mut purl = PackageUrl::new(PackageType::Nix.as_str(), name).ok()?;
1225    if let Some(version) = version {
1226        purl.with_version(version).ok()?;
1227    }
1228    Some(truncate_field(purl.to_string()))
1229}
1230
1231fn parse_nix_expr(content: &str) -> Result<Expr, String> {
1232    let tokens = Lexer::new(content).tokenize()?;
1233    Parser::new(tokens).parse()
1234}
1235
1236fn attrset_entries(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1237    match expr {
1238        Expr::AttrSet(entries) => Some(entries),
1239        _ => None,
1240    }
1241}
1242
1243fn list_items_with_scopes<'a>(
1244    expr: &'a Expr,
1245    scopes: &[&'a [(Vec<String>, Expr)]],
1246    depth: usize,
1247) -> Option<&'a [Expr]> {
1248    if depth > MAX_RECURSION_DEPTH {
1249        warn!("list_items_with_scopes exceeded MAX_RECURSION_DEPTH");
1250        return None;
1251    }
1252
1253    match expr {
1254        Expr::List(items) => Some(items),
1255        Expr::Let { bindings, body } => {
1256            let scopes = extend_scopes(scopes, bindings);
1257            list_items_with_scopes(body, &scopes, depth + 1)
1258        }
1259        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, 0)
1260            .and_then(|resolved| list_items_with_scopes(resolved, scopes, depth + 1)),
1261        Expr::Select { target, path } => resolve_select(target, path, scopes, 0)
1262            .and_then(|resolved| list_items_with_scopes(resolved, scopes, depth + 1)),
1263        _ => None,
1264    }
1265}
1266
1267fn expr_as_symbol(expr: &Expr) -> Option<String> {
1268    match expr {
1269        Expr::Symbol(value) => Some(value.clone()),
1270        _ => None,
1271    }
1272}
1273
1274fn expr_as_symbol_with_scopes(
1275    expr: &Expr,
1276    scopes: &[&[(Vec<String>, Expr)]],
1277    depth: usize,
1278) -> Option<String> {
1279    if depth > MAX_RECURSION_DEPTH {
1280        warn!("expr_as_symbol_with_scopes exceeded MAX_RECURSION_DEPTH");
1281        return None;
1282    }
1283
1284    match expr {
1285        Expr::Symbol(value) => resolve_symbol(value, scopes, 0)
1286            .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes, depth + 1))
1287            .or_else(|| Some(value.clone())),
1288        Expr::Select { target, path } => resolve_select(target, path, scopes, 0)
1289            .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes, depth + 1)),
1290        Expr::Let { bindings, body } => {
1291            let scopes = extend_scopes(scopes, bindings);
1292            expr_as_symbol_with_scopes(body, &scopes, depth + 1)
1293        }
1294        _ => expr_as_symbol(expr),
1295    }
1296}
1297
1298fn expr_as_bool(expr: &Expr) -> Option<bool> {
1299    match expr {
1300        Expr::Symbol(value) if value == "true" => Some(true),
1301        Expr::Symbol(value) if value == "false" => Some(false),
1302        _ => None,
1303    }
1304}
1305
1306fn expr_as_bool_with_scopes(
1307    expr: &Expr,
1308    scopes: &[&[(Vec<String>, Expr)]],
1309    depth: usize,
1310) -> Option<bool> {
1311    if depth > MAX_RECURSION_DEPTH {
1312        warn!("expr_as_bool_with_scopes exceeded MAX_RECURSION_DEPTH");
1313        return None;
1314    }
1315
1316    match expr {
1317        Expr::Let { bindings, body } => {
1318            let scopes = extend_scopes(scopes, bindings);
1319            expr_as_bool_with_scopes(body, &scopes, depth + 1)
1320        }
1321        Expr::Symbol(value) => resolve_symbol(value, scopes, 0)
1322            .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes, depth + 1))
1323            .or_else(|| expr_as_bool(expr)),
1324        Expr::Select { target, path } => resolve_select(target, path, scopes, 0)
1325            .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes, depth + 1)),
1326        _ => expr_as_bool(expr),
1327    }
1328}
1329
1330fn expr_as_string_with_scopes(
1331    expr: &Expr,
1332    scopes: &[&[(Vec<String>, Expr)]],
1333    depth: usize,
1334) -> Option<String> {
1335    if depth > MAX_RECURSION_DEPTH {
1336        warn!("expr_as_string_with_scopes exceeded MAX_RECURSION_DEPTH");
1337        return None;
1338    }
1339
1340    match expr {
1341        Expr::String(value) => Some(interpolate_string(value, scopes)),
1342        Expr::Symbol(value) => resolve_symbol(value, scopes, 0)
1343            .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes, depth + 1))
1344            .or_else(|| Some(value.clone())),
1345        Expr::Application(parts) => parts
1346            .last()
1347            .and_then(|expr| expr_as_string_with_scopes(expr, scopes, depth + 1)),
1348        Expr::Let { bindings, body } => {
1349            let scopes = extend_scopes(scopes, bindings);
1350            expr_as_string_with_scopes(body, &scopes, depth + 1)
1351        }
1352        Expr::Select { target, path } => resolve_select(target, path, scopes, 0)
1353            .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes, depth + 1)),
1354        _ => None,
1355    }
1356}
1357
1358fn expr_to_scalar_string_with_scopes(
1359    expr: &Expr,
1360    scopes: &[&[(Vec<String>, Expr)]],
1361    depth: usize,
1362) -> Option<String> {
1363    if depth > MAX_RECURSION_DEPTH {
1364        warn!("expr_to_scalar_string_with_scopes exceeded MAX_RECURSION_DEPTH");
1365        return None;
1366    }
1367
1368    match expr {
1369        Expr::Application(parts) => parts
1370            .last()
1371            .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes, depth + 1)),
1372        _ => expr_as_string_with_scopes(expr, scopes, depth),
1373    }
1374}
1375
1376fn find_attr<'a>(
1377    entries: &'a [(Vec<String>, Expr)],
1378    path: &[&str],
1379    depth: usize,
1380) -> Option<&'a Expr> {
1381    if depth > MAX_RECURSION_DEPTH {
1382        warn!("find_attr exceeded MAX_RECURSION_DEPTH");
1383        return None;
1384    }
1385
1386    for (key, value) in entries {
1387        if key.iter().map(String::as_str).eq(path.iter().copied()) {
1388            return Some(value);
1389        }
1390
1391        if key.len() < path.len()
1392            && key
1393                .iter()
1394                .map(String::as_str)
1395                .eq(path[..key.len()].iter().copied())
1396            && let Expr::AttrSet(child_entries) = value
1397            && let Some(found) = find_attr(child_entries, &path[key.len()..], depth + 1)
1398        {
1399            return Some(found);
1400        }
1401    }
1402
1403    None
1404}
1405
1406fn find_string_attr_with_scopes(
1407    entries: &[(Vec<String>, Expr)],
1408    path: &[&str],
1409    scopes: &[&[(Vec<String>, Expr)]],
1410) -> Option<String> {
1411    find_attr(entries, path, 0)
1412        .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes, 0))
1413        .map(truncate_field)
1414}
1415
1416fn find_mk_derivation_attrset(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1417    match expr {
1418        Expr::Application(parts) => {
1419            let is_derivation = parts
1420                .first()
1421                .and_then(expr_as_symbol)
1422                .is_some_and(|symbol| symbol.ends_with("mkDerivation"));
1423            if is_derivation {
1424                return parts.iter().rev().find_map(attrset_entries);
1425            }
1426            None
1427        }
1428        _ => None,
1429    }
1430}
1431
1432fn extend_scopes<'a>(
1433    scopes: &[NixAttrEntriesRef<'a>],
1434    bindings: NixAttrEntriesRef<'a>,
1435) -> NixScopeStack<'a> {
1436    let mut extended = scopes.to_vec();
1437    extended.push(bindings);
1438    extended
1439}
1440
1441fn root_attrset_with_scopes<'a>(
1442    expr: &'a Expr,
1443    scopes: &[NixAttrEntriesRef<'a>],
1444    depth: usize,
1445) -> Option<(NixAttrEntriesRef<'a>, NixScopeStack<'a>)> {
1446    if depth > MAX_RECURSION_DEPTH {
1447        warn!("root_attrset_with_scopes exceeded MAX_RECURSION_DEPTH");
1448        return None;
1449    }
1450
1451    match expr {
1452        Expr::AttrSet(entries) => Some((entries, scopes.to_vec())),
1453        Expr::Let { bindings, body } => {
1454            let scopes = extend_scopes(scopes, bindings);
1455            root_attrset_with_scopes(body, &scopes, depth + 1)
1456        }
1457        _ => None,
1458    }
1459}
1460
1461fn lookup_binding<'a>(scopes: &[NixAttrEntriesRef<'a>], name: &str) -> Option<&'a Expr> {
1462    scopes
1463        .iter()
1464        .rev()
1465        .find_map(|bindings| find_attr(bindings, &[name], 0))
1466}
1467
1468fn resolve_symbol<'a>(
1469    symbol: &str,
1470    scopes: &[NixAttrEntriesRef<'a>],
1471    depth: usize,
1472) -> Option<&'a Expr> {
1473    if depth > MAX_RECURSION_DEPTH {
1474        return None;
1475    }
1476
1477    let mut parts = symbol.split('.');
1478    let head = parts.next()?;
1479    let mut expr = lookup_binding(scopes, head)?;
1480    let rest = parts.collect::<Vec<_>>();
1481    if rest.is_empty() {
1482        return Some(expr);
1483    }
1484
1485    for segment in rest {
1486        expr = resolve_select(expr, &[segment.to_string()], scopes, depth + 1)?;
1487    }
1488
1489    Some(expr)
1490}
1491
1492fn resolve_select<'a>(
1493    target: &'a Expr,
1494    path: &[String],
1495    scopes: &[NixAttrEntriesRef<'a>],
1496    depth: usize,
1497) -> Option<&'a Expr> {
1498    if depth > MAX_RECURSION_DEPTH {
1499        return None;
1500    }
1501
1502    match target {
1503        Expr::AttrSet(entries) => find_attr(
1504            entries,
1505            &path.iter().map(String::as_str).collect::<Vec<_>>(),
1506            0,
1507        ),
1508        Expr::Let { bindings, body } => {
1509            let scopes = extend_scopes(scopes, bindings);
1510            resolve_select(body, path, &scopes, depth + 1)
1511        }
1512        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, depth + 1)
1513            .and_then(|resolved| resolve_select(resolved, path, scopes, depth + 1)),
1514        Expr::Select {
1515            target: inner_target,
1516            path: inner_path,
1517        } => resolve_select(inner_target, inner_path, scopes, depth + 1)
1518            .and_then(|resolved| resolve_select(resolved, path, scopes, depth + 1)),
1519        _ => None,
1520    }
1521}
1522
1523fn interpolate_string(value: &str, scopes: &[&[(Vec<String>, Expr)]]) -> String {
1524    let mut result = String::new();
1525    let mut index = 0usize;
1526
1527    while let Some(relative_start) = value[index..].find("${") {
1528        let start = index + relative_start;
1529        result.push_str(&value[index..start]);
1530
1531        let placeholder_start = start + 2;
1532        let Some(relative_end) = value[placeholder_start..].find('}') else {
1533            result.push_str(&value[start..]);
1534            return result;
1535        };
1536        let end = placeholder_start + relative_end;
1537        let placeholder = value[placeholder_start..end].trim();
1538        if !placeholder.is_empty()
1539            && placeholder
1540                .chars()
1541                .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
1542            && let Some(resolved) = resolve_symbol(placeholder, scopes, 0)
1543            && let Some(replacement) = expr_as_string_with_scopes(resolved, scopes, 0)
1544        {
1545            result.push_str(&replacement);
1546        } else {
1547            result.push_str(&value[start..=end]);
1548        }
1549
1550        index = end + 1;
1551    }
1552
1553    result.push_str(&value[index..]);
1554    result
1555}
1556
1557fn extract_default_nix_package(
1558    path: &Path,
1559    expr: &Expr,
1560    scopes: &[&[(Vec<String>, Expr)]],
1561    depth: usize,
1562) -> Result<PackageData, String> {
1563    if depth > 2 {
1564        return Err("default.nix exceeded supported local import depth".to_string());
1565    }
1566
1567    match expr {
1568        Expr::Let { bindings, body } => {
1569            let scopes = extend_scopes(scopes, bindings);
1570            extract_default_nix_package(path, body, &scopes, depth)
1571        }
1572        Expr::Application(parts) => {
1573            if let Some(derivation) = find_mk_derivation_attrset(expr) {
1574                return build_default_package_from_attrset(path, derivation, scopes);
1575            }
1576
1577            if let Some((imported_expr, imported_path)) =
1578                try_follow_local_nix_application(path, parts, scopes)
1579            {
1580                return extract_default_nix_package(
1581                    &imported_path,
1582                    &imported_expr,
1583                    &Vec::new(),
1584                    depth + 1,
1585                );
1586            }
1587
1588            if let Some(package) = parts
1589                .first()
1590                .and_then(|part| extract_flake_compat_package_from_expr(path, part, scopes, depth))
1591            {
1592                return Ok(package);
1593            }
1594
1595            Err("default.nix did not contain a supported mkDerivation call".to_string())
1596        }
1597        Expr::Select {
1598            target,
1599            path: select_path,
1600        } => {
1601            if let Some(package) =
1602                extract_flake_compat_package_from_select(path, target, select_path, scopes, depth)
1603            {
1604                return Ok(package);
1605            }
1606
1607            if let Some((imported_expr, imported_path)) =
1608                try_follow_selected_local_import(path, target, select_path, scopes)
1609            {
1610                return extract_default_nix_package(
1611                    &imported_path,
1612                    &imported_expr,
1613                    &Vec::new(),
1614                    depth + 1,
1615                );
1616            }
1617
1618            if let Some(resolved) = resolve_select(target, select_path, scopes, 0) {
1619                return extract_default_nix_package(path, resolved, scopes, depth);
1620            }
1621
1622            Err("default.nix did not contain a supported mkDerivation call".to_string())
1623        }
1624        Expr::Symbol(_) => extract_flake_compat_package_from_expr(path, expr, scopes, depth)
1625            .ok_or_else(|| "default.nix did not contain a supported mkDerivation call".to_string()),
1626        _ => Err("default.nix did not contain a supported mkDerivation call".to_string()),
1627    }
1628}
1629
1630fn build_default_package_from_attrset(
1631    path: &Path,
1632    derivation: &[(Vec<String>, Expr)],
1633    scopes: &[&[(Vec<String>, Expr)]],
1634) -> Result<PackageData, String> {
1635    let mut package = default_default_nix_package_data();
1636    package.name = find_string_attr_with_scopes(derivation, &["pname"], scopes).or_else(|| {
1637        find_string_attr_with_scopes(derivation, &["name"], scopes)
1638            .map(|name| split_derivation_name(&name).0)
1639    });
1640    package.version =
1641        find_string_attr_with_scopes(derivation, &["version"], scopes).or_else(|| {
1642            find_string_attr_with_scopes(derivation, &["name"], scopes)
1643                .and_then(|name| split_derivation_name(&name).1)
1644        });
1645    package.description =
1646        find_string_attr_with_scopes(derivation, &["meta", "description"], scopes)
1647            .or_else(|| find_string_attr_with_scopes(derivation, &["description"], scopes));
1648    package.homepage_url = find_string_attr_with_scopes(derivation, &["meta", "homepage"], scopes)
1649        .or_else(|| find_string_attr_with_scopes(derivation, &["homepage"], scopes));
1650    package.extracted_license_statement = find_attr(derivation, &["meta", "license"], 0)
1651        .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes, 0))
1652        .or_else(|| {
1653            find_attr(derivation, &["license"], 0)
1654                .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes, 0))
1655        });
1656    package.dependencies = [
1657        build_list_dependencies(derivation, "nativeBuildInputs", false, scopes),
1658        build_list_dependencies(derivation, "buildInputs", true, scopes),
1659        build_list_dependencies(derivation, "propagatedBuildInputs", true, scopes),
1660        build_list_dependencies(derivation, "checkInputs", false, scopes),
1661    ]
1662    .concat();
1663    if package.name.is_none() {
1664        package.name = fallback_name(path).map(truncate_field);
1665    }
1666    package.purl = package
1667        .name
1668        .as_deref()
1669        .and_then(|name| build_nix_purl(name, package.version.as_deref()));
1670
1671    Ok(package)
1672}
1673
1674fn try_follow_local_nix_application(
1675    path: &Path,
1676    parts: &[Expr],
1677    scopes: &[&[(Vec<String>, Expr)]],
1678) -> Option<(Expr, std::path::PathBuf)> {
1679    let head = parts.first().and_then(expr_as_symbol)?;
1680    let is_supported_wrapper = head == "import" || head.ends_with("callPackage");
1681    if !is_supported_wrapper {
1682        return None;
1683    }
1684
1685    let local_path = parts
1686        .get(1)
1687        .and_then(|expr| expr_as_symbol_with_scopes(expr, scopes, 0))?;
1688    if !is_local_nix_path(&local_path) {
1689        return None;
1690    }
1691
1692    let resolved_path = resolve_local_nix_path(path, &local_path)?;
1693    let content = read_file_to_string(&resolved_path, None).ok()?;
1694    let expr = parse_nix_expr(&content).ok()?;
1695    Some((expr, resolved_path))
1696}
1697
1698fn try_follow_selected_local_import(
1699    path: &Path,
1700    target: &Expr,
1701    select_path: &[String],
1702    scopes: &[&[(Vec<String>, Expr)]],
1703) -> Option<(Expr, std::path::PathBuf)> {
1704    let Expr::Application(parts) = target else {
1705        return None;
1706    };
1707
1708    let (imported_expr, imported_path) = try_follow_local_nix_application(path, parts, scopes)?;
1709    let selected = attrset_entries(&imported_expr).and_then(|entries| {
1710        find_attr(
1711            entries,
1712            &select_path.iter().map(String::as_str).collect::<Vec<_>>(),
1713            0,
1714        )
1715    })?;
1716    Some((selected.clone(), imported_path))
1717}
1718
1719fn extract_flake_compat_package_from_expr(
1720    path: &Path,
1721    expr: &Expr,
1722    scopes: &[&[(Vec<String>, Expr)]],
1723    depth: usize,
1724) -> Option<PackageData> {
1725    if depth > 2 {
1726        return None;
1727    }
1728
1729    match expr {
1730        Expr::Select {
1731            target,
1732            path: select_path,
1733        } => extract_flake_compat_package_from_select(path, target, select_path, scopes, depth),
1734        Expr::Let { bindings, body } => {
1735            let scopes = extend_scopes(scopes, bindings);
1736            extract_flake_compat_package_from_expr(path, body, &scopes, depth)
1737        }
1738        Expr::Symbol(symbol) => {
1739            if let Some((head, rest)) = symbol.split_once('.') {
1740                let select_path = rest.split('.').map(ToOwned::to_owned).collect::<Vec<_>>();
1741                resolve_symbol(head, scopes, 0)
1742                    .and_then(|resolved| {
1743                        extract_flake_compat_package_from_select(
1744                            path,
1745                            resolved,
1746                            &select_path,
1747                            scopes,
1748                            depth,
1749                        )
1750                    })
1751                    .or_else(|| {
1752                        let target = Expr::Symbol(head.to_string());
1753                        extract_flake_compat_package_from_select(
1754                            path,
1755                            &target,
1756                            &select_path,
1757                            scopes,
1758                            depth,
1759                        )
1760                    })
1761                    .or_else(|| {
1762                        resolve_symbol(symbol, scopes, 0).and_then(|resolved| {
1763                            extract_flake_compat_package_from_expr(path, resolved, scopes, depth)
1764                        })
1765                    })
1766            } else {
1767                resolve_symbol(symbol, scopes, 0).and_then(|resolved| {
1768                    extract_flake_compat_package_from_expr(path, resolved, scopes, depth)
1769                })
1770            }
1771        }
1772        _ => None,
1773    }
1774}
1775
1776fn extract_flake_compat_package_from_select(
1777    path: &Path,
1778    target: &Expr,
1779    select_path: &[String],
1780    scopes: &[&[(Vec<String>, Expr)]],
1781    depth: usize,
1782) -> Option<PackageData> {
1783    if depth > 2 || select_path.first().map(String::as_str) != Some("defaultNix") {
1784        return None;
1785    }
1786
1787    let source_root = resolve_flake_compat_source_root(path, target, scopes, 0)?;
1788    let mut package = default_default_nix_package_data();
1789    package.name = source_root
1790        .file_name()
1791        .and_then(|name| name.to_str())
1792        .map(ToOwned::to_owned)
1793        .map(truncate_field)
1794        .or_else(|| fallback_name(path));
1795    package.purl = package
1796        .name
1797        .as_deref()
1798        .and_then(|name| build_nix_purl(name, None));
1799    mark_flake_compat_wrapper(&mut package);
1800    Some(package)
1801}
1802
1803fn resolve_flake_compat_source_root(
1804    path: &Path,
1805    target: &Expr,
1806    scopes: &[&[(Vec<String>, Expr)]],
1807    depth: usize,
1808) -> Option<std::path::PathBuf> {
1809    if depth > 8 {
1810        return None;
1811    }
1812
1813    match target {
1814        Expr::Application(parts) => source_root_from_flake_compat_application(path, parts, scopes),
1815        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, depth + 1).and_then(|resolved| {
1816            resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)
1817        }),
1818        Expr::Let { bindings, body } => {
1819            let scopes = extend_scopes(scopes, bindings);
1820            resolve_flake_compat_source_root(path, body, &scopes, depth + 1)
1821        }
1822        Expr::Select {
1823            target: inner_target,
1824            path: inner_path,
1825        } => resolve_select(inner_target, inner_path, scopes, depth + 1).and_then(|resolved| {
1826            resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)
1827        }),
1828        _ => None,
1829    }
1830}
1831
1832fn source_root_from_flake_compat_application(
1833    path: &Path,
1834    parts: &[Expr],
1835    scopes: &[&[(Vec<String>, Expr)]],
1836) -> Option<std::path::PathBuf> {
1837    let head = parts.first().and_then(expr_as_symbol)?;
1838    if head != "import" {
1839        return None;
1840    }
1841
1842    let import_path = parts
1843        .get(1)
1844        .and_then(|expr| expr_as_symbol_with_scopes(expr, scopes, 0))?;
1845    if !is_local_nix_path(&import_path) {
1846        return None;
1847    }
1848
1849    let args = parts.iter().find_map(attrset_entries)?;
1850    let src_value = find_attr(args, &["src"], 0)
1851        .and_then(|expr| expr_as_symbol_with_scopes(expr, scopes, 0))?;
1852    if !is_local_path(&src_value) {
1853        return None;
1854    }
1855
1856    resolve_local_path(path, &src_value)
1857}
1858
1859fn is_local_path(value: &str) -> bool {
1860    value.starts_with("./") || value.starts_with("../")
1861}
1862
1863fn is_local_nix_path(value: &str) -> bool {
1864    is_local_path(value) && value.ends_with(".nix")
1865}
1866
1867fn resolve_local_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1868    let base = path.parent()?;
1869    let resolved = base.join(value);
1870    resolved.exists().then_some(resolved)
1871}
1872
1873fn resolve_local_nix_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1874    resolve_local_path(path, value).filter(|resolved| resolved.is_file())
1875}
1876
1877fn extract_flake_compat_default_package_from_content(
1878    path: &Path,
1879    content: &str,
1880) -> Result<PackageData, String> {
1881    if !content.contains("defaultNix") || !content.contains("flake-compat.nix") {
1882        return Err("default.nix did not contain a supported mkDerivation call".to_string());
1883    }
1884
1885    let src_value = extract_local_flake_compat_src_value(content).unwrap_or("./.".to_string());
1886    let mut package = default_default_nix_package_data();
1887    package.name = normalize_local_source_root(path, &src_value)
1888        .and_then(|source_root| {
1889            source_root
1890                .file_name()
1891                .and_then(|name| name.to_str())
1892                .filter(|name| *name != ".")
1893                .map(ToOwned::to_owned)
1894        })
1895        .map(truncate_field)
1896        .or_else(|| fallback_name(path));
1897    if package.name.is_none() {
1898        return Err("default.nix did not contain a supported mkDerivation call".to_string());
1899    }
1900    package.purl = package
1901        .name
1902        .as_deref()
1903        .and_then(|name| build_nix_purl(name, None));
1904    mark_flake_compat_wrapper(&mut package);
1905    Ok(package)
1906}
1907
1908fn mark_flake_compat_wrapper(package: &mut PackageData) {
1909    let mut extra_data = package.extra_data.clone().unwrap_or_default();
1910    extra_data.insert(
1911        "nix_wrapper_kind".to_string(),
1912        JsonValue::String("flake_compat".to_string()),
1913    );
1914    package.extra_data = Some(extra_data);
1915}
1916
1917fn extract_local_flake_compat_src_value(content: &str) -> Option<String> {
1918    let src_index = content.find("src")?;
1919    let after_src = &content[src_index + 3..];
1920    let equals_index = after_src.find('=')?;
1921    let remainder = after_src[equals_index + 1..].trim_start();
1922    let end_index = remainder.find([';', '}', '\n']).unwrap_or(remainder.len());
1923    let candidate = remainder[..end_index].trim();
1924    if is_local_path(candidate) {
1925        Some(candidate.to_string())
1926    } else {
1927        None
1928    }
1929}
1930
1931fn normalize_local_source_root(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1932    match value {
1933        "." | "./." => path.parent().map(|parent| parent.to_path_buf()),
1934        _ if value.ends_with("/.") => resolve_local_path(path, value.trim_end_matches("/.")),
1935        _ => resolve_local_path(path, value),
1936    }
1937}
1938
1939fn split_derivation_name(name: &str) -> (String, Option<String>) {
1940    let mut parts = name.rsplitn(2, '-');
1941    let maybe_version = parts
1942        .next()
1943        .filter(|value| value.chars().any(|ch| ch.is_ascii_digit()));
1944    let maybe_name = parts.next();
1945
1946    match (maybe_name, maybe_version) {
1947        (Some(package_name), Some(version)) => {
1948            (package_name.to_string(), Some(version.to_string()))
1949        }
1950        _ => (name.to_string(), None),
1951    }
1952}
1953
1954fn default_flake_package_data() -> PackageData {
1955    PackageData {
1956        package_type: Some(PackageType::Nix),
1957        primary_language: Some("Nix".to_string()),
1958        datasource_id: Some(DatasourceId::NixFlakeNix),
1959        ..Default::default()
1960    }
1961}
1962
1963fn default_flake_lock_package_data() -> PackageData {
1964    PackageData {
1965        package_type: Some(PackageType::Nix),
1966        primary_language: Some("JSON".to_string()),
1967        datasource_id: Some(DatasourceId::NixFlakeLock),
1968        ..Default::default()
1969    }
1970}
1971
1972fn default_default_nix_package_data() -> PackageData {
1973    PackageData {
1974        package_type: Some(PackageType::Nix),
1975        primary_language: Some("Nix".to_string()),
1976        datasource_id: Some(DatasourceId::NixDefaultNix),
1977        ..Default::default()
1978    }
1979}
1980
1981crate::register_parser!(
1982    "Nix flake manifest",
1983    &["**/flake.nix"],
1984    "nix",
1985    "Nix",
1986    Some("https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-flake.html"),
1987);
1988
1989crate::register_parser!(
1990    "Nix flake lockfile",
1991    &["**/flake.lock"],
1992    "nix",
1993    "JSON",
1994    Some("https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-flake.html"),
1995);
1996
1997crate::register_parser!(
1998    "Nix derivation manifest",
1999    &["**/default.nix"],
2000    "nix",
2001    "Nix",
2002    Some("https://nix.dev/manual/nix/stable/language/derivations.html"),
2003);