Skip to main content

provenant/parsers/
nix.rs

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