Skip to main content

lintian_overrides/
lib.rs

1use rowan::{GreenNode, GreenNodeBuilder};
2use std::fmt;
3
4/// Check if an info string matches a pattern with wildcards
5///
6/// Supports `*` wildcards in patterns like `[debian/copyright:*]` matching `[debian/copyright:31]`
7/// The asterisk matches arbitrary strings similar to shell wildcards.
8pub fn info_matches(pattern: &str, value: &str) -> bool {
9    if pattern == value {
10        return true;
11    }
12
13    // Check if pattern contains wildcards
14    if !pattern.contains('*') {
15        return false;
16    }
17
18    // Split pattern by wildcards
19    let parts: Vec<&str> = pattern.split('*').collect();
20
21    // Check prefix (before first *)
22    if !parts[0].is_empty() && !value.starts_with(parts[0]) {
23        return false;
24    }
25
26    // Check suffix (after last *)
27    if !parts[parts.len() - 1].is_empty() && !value.ends_with(parts[parts.len() - 1]) {
28        return false;
29    }
30
31    // Check middle parts appear in order
32    let mut pos = parts[0].len();
33    for part in &parts[1..parts.len() - 1] {
34        if part.is_empty() {
35            continue;
36        }
37        if let Some(found) = value[pos..].find(part) {
38            pos += found + part.len();
39        } else {
40            return false;
41        }
42    }
43
44    true
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48#[allow(non_camel_case_types)]
49#[repr(u16)]
50/// Syntax kinds for lintian override files
51pub enum SyntaxKind {
52    /// Whitespace token
53    WHITESPACE = 0,
54    /// Comment token
55    COMMENT,
56    /// Package name token
57    PACKAGE_NAME,
58    /// Left bracket token for Architecture
59    L_BRACKET,
60    /// Right bracket token for Architecture
61    R_BRACKET,
62    /// Single architecture token
63    ARCH,
64    /// Colon token
65    COLON,
66    /// Package type token
67    PACKAGE_TYPE,
68    /// Tag token
69    TAG,
70    /// Info token
71    INFO,
72    /// Newline token
73    NEWLINE,
74    /// Root node
75    ROOT,
76    /// Override line node
77    OVERRIDE_LINE,
78    /// Package specification node
79    PACKAGE_SPEC,
80    /// Error node
81    ERROR,
82}
83
84use SyntaxKind::*;
85
86impl From<SyntaxKind> for rowan::SyntaxKind {
87    fn from(kind: SyntaxKind) -> Self {
88        Self(kind as u16)
89    }
90}
91
92/// Language type for the lintian override parser
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
94pub enum Lang {}
95
96impl rowan::Language for Lang {
97    type Kind = SyntaxKind;
98
99    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
100        assert!(raw.0 <= ERROR as u16);
101        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
102    }
103
104    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
105        kind.into()
106    }
107}
108
109/// Syntax node type for lintian overrides
110pub type SyntaxNode = rowan::SyntaxNode<Lang>;
111/// Syntax token type for lintian overrides
112pub type SyntaxToken = rowan::SyntaxToken<Lang>;
113/// Syntax element type for lintian overrides
114pub type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
115
116/// The result of parsing a lintian-overrides file.
117///
118/// `PartialEq` / `Eq` compare the underlying `GreenNode` (cheap —
119/// rowan green nodes are interned) and the errors, so this type
120/// can be stored in a Salsa database. The manual `Send` / `Sync`
121/// impls assert that `Parse` itself is thread-safe even when `T`
122/// (the AST node type) wraps a non-thread-safe `SyntaxNode`: the
123/// only field carrying real data is the `GreenNode`, which is
124/// thread-safe. The phantom `T` exists for type-tagging only.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct Parse<T> {
127    green: GreenNode,
128    errors: Vec<String>,
129    _phantom: std::marker::PhantomData<T>,
130}
131
132// SAFETY: The only data Parse holds is the GreenNode (thread-safe
133// — that's the whole point of rowan's red-green split) and the
134// errors Vec. The PhantomData<T> contributes no runtime data.
135unsafe impl<T> Send for Parse<T> {}
136unsafe impl<T> Sync for Parse<T> {}
137
138impl<T> Parse<T> {
139    fn new(green: GreenNode, errors: Vec<String>) -> Self {
140        Parse {
141            green,
142            errors,
143            _phantom: std::marker::PhantomData,
144        }
145    }
146
147    /// Get the syntax tree
148    pub fn syntax(&self) -> SyntaxNode {
149        SyntaxNode::new_root(self.green.clone())
150    }
151
152    /// Get the parse errors
153    pub fn errors(&self) -> &[String] {
154        &self.errors
155    }
156
157    /// Convert to result, returning errors if any
158    pub fn ok(self) -> Result<T, Vec<String>>
159    where
160        T: AstNode,
161    {
162        if self.errors.is_empty() {
163            Ok(T::cast(self.syntax()).unwrap())
164        } else {
165            Err(self.errors)
166        }
167    }
168
169    /// Return the parsed tree even when there are errors.
170    ///
171    /// Lintian-overrides parsing is resilient: the parser always
172    /// produces a green tree (well-formed lines + recovered tokens
173    /// for malformed ones), so consumers that want partial output —
174    /// LSP semantic tokens, completion lookups, hover — should walk
175    /// `tree()` rather than throwing the whole document away on the
176    /// first parse error via `ok()`.
177    pub fn tree(&self) -> T
178    where
179        T: AstNode,
180    {
181        T::cast(self.syntax()).expect("root node has wrong type")
182    }
183}
184
185/// Trait for AST nodes
186pub trait AstNode: Clone {
187    /// Cast a syntax node to this AST node type
188    fn cast(syntax: SyntaxNode) -> Option<Self>
189    where
190        Self: Sized;
191
192    /// Get the underlying syntax node
193    fn syntax(&self) -> &SyntaxNode;
194}
195
196/// The root node of a lintian-overrides file
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct LintianOverrides {
199    syntax: SyntaxNode,
200}
201
202impl AstNode for LintianOverrides {
203    fn cast(syntax: SyntaxNode) -> Option<Self> {
204        if syntax.kind() == ROOT {
205            Some(Self { syntax })
206        } else {
207            None
208        }
209    }
210
211    fn syntax(&self) -> &SyntaxNode {
212        &self.syntax
213    }
214}
215
216impl LintianOverrides {
217    /// Capture an independent snapshot of this lintian-overrides file.
218    ///
219    /// The returned value shares the underlying immutable green-node data
220    /// with `self` at the time of the call, but lives in its own mutable
221    /// tree: subsequent mutations to `self` do not propagate to the snapshot.
222    /// Pair with [`Self::tree_eq`] to detect later mutations.
223    pub fn snapshot(&self) -> Self {
224        LintianOverrides {
225            syntax: SyntaxNode::new_root_mut(self.syntax.green().into_owned()),
226        }
227    }
228
229    /// Returns true iff the syntax trees of `self` and `other` are
230    /// value-equal. An O(1) pointer-identity fast path makes this free for
231    /// trees that still share state with a recent `snapshot()`.
232    pub fn tree_eq(&self, other: &Self) -> bool {
233        let a = self.syntax.green();
234        let b = other.syntax.green();
235        let a_ref: &rowan::GreenNodeData = &a;
236        let b_ref: &rowan::GreenNodeData = &b;
237        std::ptr::eq(a_ref as *const _, b_ref as *const _) || a_ref == b_ref
238    }
239
240    /// Parse a lintian-overrides file
241    pub fn parse(text: &str) -> Parse<Self> {
242        let (green, errors) = parse_lintian_overrides(text);
243        Parse::new(green, errors)
244    }
245
246    /// Get all override lines
247    pub fn lines(&self) -> impl Iterator<Item = OverrideLine> + '_ {
248        self.syntax.children().filter_map(OverrideLine::cast)
249    }
250
251    /// Convert back to text
252    pub fn text(&self) -> String {
253        self.syntax.text().to_string()
254    }
255
256    /// Get a reference to the underlying syntax node
257    pub fn syntax_node(&self) -> &SyntaxNode {
258        &self.syntax
259    }
260}
261
262impl fmt::Display for LintianOverrides {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        write!(f, "{}", self.syntax.text())
265    }
266}
267
268/// A single override line
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct OverrideLine {
271    syntax: SyntaxNode,
272}
273
274impl AstNode for OverrideLine {
275    fn cast(syntax: SyntaxNode) -> Option<Self> {
276        if syntax.kind() == OVERRIDE_LINE {
277            Some(Self { syntax })
278        } else {
279            None
280        }
281    }
282
283    fn syntax(&self) -> &SyntaxNode {
284        &self.syntax
285    }
286}
287
288impl OverrideLine {
289    /// Check if this line is a comment
290    pub fn is_comment(&self) -> bool {
291        self.syntax
292            .children_with_tokens()
293            .any(|it| matches!(it.as_token(), Some(token) if token.kind() == COMMENT))
294    }
295
296    /// Check if this line is empty
297    pub fn is_empty(&self) -> bool {
298        self.syntax
299            .children_with_tokens()
300            .all(|it| matches!(it.as_token(), Some(token) if token.kind() == WHITESPACE || token.kind() == NEWLINE))
301    }
302
303    /// Get the package specification if present
304    pub fn package_spec(&self) -> Option<PackageSpec> {
305        self.syntax.children().find_map(PackageSpec::cast)
306    }
307
308    /// Get the tag token
309    pub fn tag(&self) -> Option<SyntaxToken> {
310        self.syntax
311            .children_with_tokens()
312            .filter_map(|it| it.into_token())
313            .find(|it| it.kind() == TAG)
314    }
315
316    /// Get the source range of the tag token, if present.
317    pub fn tag_range(&self) -> Option<rowan::TextRange> {
318        self.tag().map(|t| t.text_range())
319    }
320
321    /// Get the source range of this override line.
322    pub fn text_range(&self) -> rowan::TextRange {
323        self.syntax.text_range()
324    }
325
326    /// Get the info text
327    pub fn info(&self) -> Option<String> {
328        let tokens: Vec<_> = self
329            .syntax
330            .children_with_tokens()
331            .filter_map(|it| it.into_token())
332            .filter(|it| it.kind() == INFO)
333            .collect();
334
335        if tokens.is_empty() {
336            None
337        } else {
338            Some(
339                tokens
340                    .iter()
341                    .map(|t| t.text())
342                    .collect::<Vec<_>>()
343                    .join(" "),
344            )
345        }
346    }
347
348    /// Get the source range spanned by the info tokens, if any are present.
349    ///
350    /// When multiple `INFO` tokens are present the range spans from the start
351    /// of the first to the end of the last.
352    pub fn info_range(&self) -> Option<rowan::TextRange> {
353        let mut tokens = self
354            .syntax
355            .children_with_tokens()
356            .filter_map(|it| it.into_token())
357            .filter(|it| it.kind() == INFO);
358        let first = tokens.next()?;
359        let last = tokens.last().unwrap_or_else(|| first.clone());
360        Some(first.text_range().cover(last.text_range()))
361    }
362
363    /// Get the package name from the package spec, if present.
364    pub fn package(&self) -> Option<String> {
365        self.package_spec()?.package_name()
366    }
367
368    /// Get the text representation of this line
369    pub fn text(&self) -> String {
370        self.syntax.text().to_string()
371    }
372
373    /// Get the package type from the package spec (e.g., "source", "binary", "udeb")
374    /// The package spec can be in format "package-name type:", "package-name [ archlist ] type", or just "type:"
375    pub fn package_type(&self) -> Option<String> {
376        self.package_spec()?.package_type()
377    }
378
379    /// Check if this override line matches a given issue described by its components.
380    ///
381    /// # Arguments
382    /// * `issue_tag` - The lintian tag to match against
383    /// * `issue_package` - The package name to match against
384    /// * `issue_package_type` - The package type (e.g. "source", "binary") to match against
385    /// * `issue_info` - Additional info to match against (supports wildcard matching)
386    pub fn matches(
387        &self,
388        issue_tag: Option<&str>,
389        issue_package: Option<&str>,
390        issue_package_type: Option<&str>,
391        issue_info: Option<&str>,
392    ) -> bool {
393        // Check if tag matches
394        if let Some(tag) = self.tag() {
395            if Some(tag.text()) != issue_tag {
396                return false;
397            }
398        } else {
399            return false;
400        }
401
402        // Check package name and/or type if specified in override
403        if let Some(pkg_spec) = self.package_spec() {
404            // Match on package name if the override specifies one.
405            if let Some(pkg_name) = pkg_spec.package_name() {
406                if Some(pkg_name.as_str()) != issue_package {
407                    return false;
408                }
409            }
410            // Match on package type if the override specifies one.
411            if let Some(pkg_type) = pkg_spec.package_type() {
412                if Some(pkg_type.as_str()) != issue_package_type {
413                    return false;
414                }
415            }
416        }
417
418        // Check info if present on the issue
419        if let Some(our_info) = issue_info {
420            if let Some(override_info) = self.info() {
421                let override_info = override_info.trim();
422                if !info_matches(override_info, our_info) {
423                    return false;
424                }
425            }
426        }
427
428        true
429    }
430}
431
432/// Package specification (e.g., "package:" or "binary:")
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct PackageSpec {
435    syntax: SyntaxNode,
436}
437
438impl AstNode for PackageSpec {
439    fn cast(syntax: SyntaxNode) -> Option<Self> {
440        if syntax.kind() == PACKAGE_SPEC {
441            Some(Self { syntax })
442        } else {
443            None
444        }
445    }
446
447    fn syntax(&self) -> &SyntaxNode {
448        &self.syntax
449    }
450}
451
452impl PackageSpec {
453    /// Get the package name
454    pub fn package_name(&self) -> Option<String> {
455        self.syntax
456            .children_with_tokens()
457            .filter_map(|it| it.into_token())
458            .find(|it| it.kind() == PACKAGE_NAME)
459            .map(|t| t.text().to_string())
460    }
461
462    /// Get the package type (source or binary)
463    pub fn package_type(&self) -> Option<String> {
464        self.syntax
465            .children_with_tokens()
466            .filter_map(|it| it.into_token())
467            .find(|it| it.kind() == PACKAGE_TYPE)
468            .map(|t| t.text().to_string())
469    }
470
471    /// Get all architecture tokens inside the bracket list.
472    pub fn arch_list(&self) -> Vec<String> {
473        self.syntax
474            .children_with_tokens()
475            .filter_map(|it| it.into_token())
476            .filter(|it| it.kind() == ARCH)
477            .map(|t| t.text().to_string())
478            .collect()
479    }
480}
481
482/// Parse a lintian-overrides file
483fn parse_lintian_overrides(text: &str) -> (GreenNode, Vec<String>) {
484    let mut builder = GreenNodeBuilder::new();
485    let mut errors = Vec::new();
486
487    builder.start_node(ROOT.into());
488
489    // split_inclusive keeps the trailing '\n' on each line if it was present,
490    // so we only emit a NEWLINE token when the source actually had one and
491    // round-trip back to exactly the input text.
492    for raw_line in text.split_inclusive('\n') {
493        let (line, has_newline) = match raw_line.strip_suffix('\n') {
494            Some(stripped) => (stripped, true),
495            None => (raw_line, false),
496        };
497        parse_line(&mut builder, line, &mut errors);
498        if has_newline {
499            builder.token(NEWLINE.into(), "\n");
500        }
501    }
502
503    builder.finish_node();
504    (builder.finish(), errors)
505}
506
507fn parse_line(builder: &mut GreenNodeBuilder, line: &str, _errors: &mut Vec<String>) {
508    builder.start_node(OVERRIDE_LINE.into());
509
510    // Handle leading whitespace
511    let trimmed_start = line.trim_start();
512    let leading_ws = &line[..line.len() - trimmed_start.len()];
513    if !leading_ws.is_empty() {
514        builder.token(WHITESPACE.into(), leading_ws);
515    }
516
517    // Check for comment
518    if trimmed_start.starts_with('#') {
519        builder.token(COMMENT.into(), trimmed_start);
520        builder.finish_node();
521        return;
522    }
523
524    // Empty line
525    if trimmed_start.is_empty() {
526        builder.finish_node();
527        return;
528    }
529
530    // Parse the override line
531    let mut current_start = 0;
532
533    // First, check if we have a package spec by looking for a colon
534    // The package spec format is "package-name:", "package-name type:" or "package-name [ archlist ] type"
535    // We need to distinguish this from info that may contain colons (e.g., "line 51:")
536    // A package spec will have:
537    // 1. A colon followed by whitespace or end-of-line
538    // 2. The part before the colon should be a reasonable package spec (1-2 words)
539    let mut has_package_spec = false;
540    let mut colon_pos = 0;
541
542    if let Some(pos) = trimmed_start.find(':') {
543        // Check if the colon is followed by whitespace or is at the end
544        let after_colon = &trimmed_start[pos + 1..];
545        if after_colon.is_empty() || after_colon.starts_with(char::is_whitespace) {
546            // Check if the part before the colon looks like a package spec
547            let before_colon = &trimmed_start[..pos];
548            if is_valid_package_spec(before_colon) {
549                // This looks like a valid package spec
550                has_package_spec = true;
551                colon_pos = pos;
552            }
553        }
554    }
555
556    if has_package_spec {
557        // Found package spec - parse it into package name, optional arch list, and optionally package type
558        builder.start_node(PACKAGE_SPEC.into());
559
560        parse_package_spec_tokens(builder, &trimmed_start[..colon_pos]);
561
562        builder.token(COLON.into(), ":");
563        builder.finish_node();
564
565        current_start = colon_pos + 1;
566
567        // Skip any whitespace after colon
568        let after_colon = &trimmed_start[current_start..];
569        let trimmed_after = after_colon.trim_start();
570        let ws_len = after_colon.len() - trimmed_after.len();
571        if ws_len > 0 {
572            builder.token(WHITESPACE.into(), &after_colon[..ws_len]);
573            current_start += ws_len;
574        }
575    }
576
577    // The remainder is: [whitespace] tag [whitespace info-spanning-the-rest].
578    // Info may itself contain whitespace, so we don't tokenise inside it —
579    // it's a single INFO token from its first byte to the end of `rest`.
580    let rest = &trimmed_start[current_start..];
581    let ws_end = rest.len() - rest.trim_start().len();
582    if ws_end > 0 {
583        builder.token(WHITESPACE.into(), &rest[..ws_end]);
584    }
585    let after_ws = &rest[ws_end..];
586    if after_ws.is_empty() {
587        builder.finish_node();
588        return;
589    }
590    let tag_end = after_ws.find(char::is_whitespace).unwrap_or(after_ws.len());
591    builder.token(TAG.into(), &after_ws[..tag_end]);
592    let after_tag = &after_ws[tag_end..];
593    if after_tag.is_empty() {
594        builder.finish_node();
595        return;
596    }
597    let info_start = after_tag.len() - after_tag.trim_start().len();
598    if info_start > 0 {
599        builder.token(WHITESPACE.into(), &after_tag[..info_start]);
600    }
601    let info = &after_tag[info_start..];
602    if !info.is_empty() {
603        builder.token(INFO.into(), info);
604    }
605
606    builder.finish_node();
607}
608
609/// Check whether the text before a colon is a valid package spec.
610fn is_valid_package_spec(before_colon: &str) -> bool {
611    let mut rest = before_colon.trim();
612    if rest.is_empty() {
613        return false;
614    }
615
616    // Optional package name - any word that is not an arch list opener.
617    if !rest.starts_with('[') {
618        let end = rest
619            .find(|c: char| c.is_whitespace() || c == '[')
620            .unwrap_or(rest.len());
621        rest = rest[end..].trim_start();
622    }
623
624    // Optional architecture list enclosed in brackets.
625    if rest.starts_with('[') {
626        match rest.find(']') {
627            Some(end) => rest = rest[end + 1..].trim_start(),
628            None => return false, // Unclosed bracket
629        }
630    }
631
632    // Optional package type keyword.
633    if !rest.is_empty() {
634        let word = rest.split_whitespace().next().unwrap_or("");
635        if word != "source" && word != "binary" && word != "udeb" {
636            return false;
637        }
638        rest = rest[word.len()..].trim_start();
639    }
640
641    // Nothing should remain after the optional fields.
642    rest.is_empty()
643}
644
645/// Emit CST tokens for the content of a package spec (everything before the colon).
646fn parse_package_spec_tokens(builder: &mut GreenNodeBuilder, spec: &str) {
647    let mut rest = spec;
648
649    // Optional package name - any word before whitespace or '['.
650    let trimmed = rest.trim_start();
651    let leading_ws = &rest[..rest.len() - trimmed.len()];
652    if !leading_ws.is_empty() {
653        builder.token(WHITESPACE.into(), leading_ws);
654    }
655    rest = trimmed;
656
657    if !rest.is_empty() && !rest.starts_with('[') {
658        let end = rest
659            .find(|c: char| c.is_whitespace() || c == '[')
660            .unwrap_or(rest.len());
661        builder.token(PACKAGE_NAME.into(), &rest[..end]);
662        rest = &rest[end..];
663    }
664
665    // Whitespace between package name and '[' or type.
666    let trimmed = rest.trim_start();
667    let ws = &rest[..rest.len() - trimmed.len()];
668    if !ws.is_empty() {
669        builder.token(WHITESPACE.into(), ws);
670    }
671    rest = trimmed;
672
673    // Optional architecture list
674    if rest.starts_with('[') {
675        if let Some(end) = rest.find(']') {
676            builder.token(L_BRACKET.into(), "[");
677
678            // Emit each arch token inside the brackets, preserving whitespace.
679            let mut arch_rest = &rest[1..end];
680            loop {
681                let trimmed_arch = arch_rest.trim_start();
682                let ws = &arch_rest[..arch_rest.len() - trimmed_arch.len()];
683                if !ws.is_empty() {
684                    builder.token(WHITESPACE.into(), ws);
685                }
686                arch_rest = trimmed_arch;
687                if arch_rest.is_empty() {
688                    break;
689                }
690                let arch_end = arch_rest
691                    .find(char::is_whitespace)
692                    .unwrap_or(arch_rest.len());
693                builder.token(ARCH.into(), &arch_rest[..arch_end]);
694                arch_rest = &arch_rest[arch_end..];
695            }
696
697            builder.token(R_BRACKET.into(), "]");
698            rest = &rest[end + 1..];
699        }
700    }
701
702    // Whitespace between ']' and type.
703    let trimmed = rest.trim_start();
704    let ws = &rest[..rest.len() - trimmed.len()];
705    if !ws.is_empty() {
706        builder.token(WHITESPACE.into(), ws);
707    }
708    rest = trimmed;
709
710    // Optional package type: source, binary, or udeb.
711    if !rest.is_empty() {
712        let word = rest.split_whitespace().next().unwrap_or("");
713        if !word.is_empty() {
714            builder.token(PACKAGE_TYPE.into(), word);
715        }
716    }
717}
718
719/// Builder for creating/modifying lintian-overrides files
720pub struct LintianOverridesBuilder<'a> {
721    builder: GreenNodeBuilder<'a>,
722}
723
724impl<'a> LintianOverridesBuilder<'a> {
725    /// Create a new builder
726    pub fn new() -> Self {
727        let mut builder = GreenNodeBuilder::new();
728        builder.start_node(ROOT.into());
729        Self { builder }
730    }
731
732    /// Add a comment line
733    pub fn add_comment(&mut self, text: &str) -> &mut Self {
734        self.builder.start_node(OVERRIDE_LINE.into());
735        self.builder.token(COMMENT.into(), text);
736        self.builder.finish_node();
737        self.builder.token(NEWLINE.into(), "\n");
738        self
739    }
740
741    /// Add an override line
742    pub fn add_override(
743        &mut self,
744        package: Option<&str>,
745        tag: &str,
746        info: Option<&str>,
747    ) -> &mut Self {
748        self.builder.start_node(OVERRIDE_LINE.into());
749
750        if let Some(pkg) = package {
751            self.builder.start_node(PACKAGE_SPEC.into());
752            self.builder.token(PACKAGE_NAME.into(), pkg);
753            self.builder.token(COLON.into(), ":");
754            self.builder.finish_node();
755            self.builder.token(WHITESPACE.into(), " ");
756        }
757
758        self.builder.token(TAG.into(), tag);
759
760        if let Some(info_text) = info {
761            self.builder.token(WHITESPACE.into(), " ");
762            self.builder.token(INFO.into(), info_text);
763        }
764
765        self.builder.finish_node();
766        self.builder.token(NEWLINE.into(), "\n");
767        self
768    }
769
770    /// Finish building and return the LintianOverrides
771    pub fn finish(mut self) -> LintianOverrides {
772        self.builder.finish_node();
773        let green = self.builder.finish();
774        LintianOverrides {
775            syntax: SyntaxNode::new_root(green),
776        }
777    }
778}
779
780impl<'a> Default for LintianOverridesBuilder<'a> {
781    fn default() -> Self {
782        Self::new()
783    }
784}
785
786/// Copy a syntax node into a green node builder
787pub fn copy_node(builder: &mut GreenNodeBuilder, node: &SyntaxNode) {
788    builder.start_node(node.kind().into());
789    for child in node.children_with_tokens() {
790        match child {
791            rowan::NodeOrToken::Token(token) => {
792                builder.token(token.kind().into(), token.text());
793            }
794            rowan::NodeOrToken::Node(child_node) => {
795                copy_node(builder, &child_node);
796            }
797        }
798    }
799    builder.finish_node();
800}
801
802/// Filter override lines based on a predicate
803pub fn filter_overrides<F>(overrides: &LintianOverrides, mut predicate: F) -> LintianOverrides
804where
805    F: FnMut(&OverrideLine) -> bool,
806{
807    let mut builder = GreenNodeBuilder::new();
808    builder.start_node(ROOT.into());
809
810    for line_node in overrides.syntax.children() {
811        if line_node.kind() == OVERRIDE_LINE {
812            let line = OverrideLine {
813                syntax: line_node.clone(),
814            };
815
816            if predicate(&line) {
817                copy_node(&mut builder, &line_node);
818                builder.token(NEWLINE.into(), "\n");
819            }
820        }
821    }
822
823    builder.finish_node();
824    let green = builder.finish();
825    LintianOverrides {
826        syntax: SyntaxNode::new_root(green),
827    }
828}
829
830/// Rebuild `overrides` with each line's TAG token rewritten by `rename`.
831///
832/// Lines whose tag is unchanged keep their original tokens (whitespace,
833/// comments, package spec) verbatim — only the TAG token is replaced.
834/// `rename` is invoked once per override line that has a tag, and should
835/// return the new tag text, or `None` to leave the tag as-is.
836///
837/// # Example
838/// ```
839/// use lintian_overrides::{LintianOverrides, rename_tags};
840/// let parsed = LintianOverrides::parse("# keep\nold-tag info\n");
841/// let overrides = parsed.ok().unwrap();
842/// let renamed = rename_tags(&overrides, |tag| {
843///     if tag == "old-tag" { Some("new-tag".to_string()) } else { None }
844/// });
845/// assert_eq!(renamed.text(), "# keep\nnew-tag info\n");
846/// ```
847pub fn rename_tags<F>(overrides: &LintianOverrides, mut rename: F) -> LintianOverrides
848where
849    F: FnMut(&str) -> Option<String>,
850{
851    let mut builder = GreenNodeBuilder::new();
852    builder.start_node(ROOT.into());
853
854    for child in overrides.syntax.children_with_tokens() {
855        match child {
856            rowan::NodeOrToken::Node(node) if node.kind() == OVERRIDE_LINE => {
857                let line = OverrideLine {
858                    syntax: node.clone(),
859                };
860                let new_tag = line.tag().and_then(|t| rename(t.text()));
861                if let Some(new_tag) = new_tag {
862                    builder.start_node(OVERRIDE_LINE.into());
863                    for element in line.syntax.children_with_tokens() {
864                        match element {
865                            rowan::NodeOrToken::Token(token) if token.kind() == TAG => {
866                                builder.token(TAG.into(), &new_tag);
867                            }
868                            rowan::NodeOrToken::Token(token) => {
869                                builder.token(token.kind().into(), token.text());
870                            }
871                            rowan::NodeOrToken::Node(child_node) => {
872                                copy_node(&mut builder, &child_node);
873                            }
874                        }
875                    }
876                    builder.finish_node();
877                } else {
878                    copy_node(&mut builder, &node);
879                }
880            }
881            rowan::NodeOrToken::Node(node) => {
882                copy_node(&mut builder, &node);
883            }
884            rowan::NodeOrToken::Token(token) => {
885                builder.token(token.kind().into(), token.text());
886            }
887        }
888    }
889
890    builder.finish_node();
891    let green = builder.finish();
892    LintianOverrides {
893        syntax: SyntaxNode::new_root(green),
894    }
895}
896
897/// Map override lines using a transformation function
898/// Returns a new LintianOverrides with the lines transformed by the function
899/// If the function returns None, the original line is kept unchanged
900pub fn map_overrides<F>(overrides: &LintianOverrides, mut transform: F) -> LintianOverrides
901where
902    F: FnMut(&OverrideLine) -> Option<(Option<String>, Option<String>, String, Option<String>)>,
903{
904    let mut builder = GreenNodeBuilder::new();
905    builder.start_node(ROOT.into());
906
907    for line in overrides.lines() {
908        // Try to transform the line
909        if let Some((package, package_type, tag, info)) = transform(&line) {
910            // Build a new override line with the transformed values
911            builder.start_node(OVERRIDE_LINE.into());
912
913            if let Some(pkg) = package {
914                builder.start_node(PACKAGE_SPEC.into());
915                builder.token(PACKAGE_NAME.into(), &pkg);
916                if let Some(ptype) = package_type {
917                    builder.token(WHITESPACE.into(), " ");
918                    builder.token(PACKAGE_TYPE.into(), &ptype);
919                }
920                builder.token(COLON.into(), ":");
921                builder.finish_node();
922                builder.token(WHITESPACE.into(), " ");
923            }
924
925            builder.token(TAG.into(), &tag);
926
927            if let Some(info_text) = info {
928                builder.token(WHITESPACE.into(), " ");
929                builder.token(INFO.into(), &info_text);
930            }
931
932            builder.finish_node();
933        } else {
934            // Keep the original line unchanged
935            copy_node(&mut builder, line.syntax());
936        }
937        builder.token(NEWLINE.into(), "\n");
938    }
939
940    builder.finish_node();
941    let green = builder.finish();
942    LintianOverrides {
943        syntax: SyntaxNode::new_root(green),
944    }
945}
946
947/// Find all lintian-overrides files in a debian directory
948pub fn find_override_files(base_path: &std::path::Path) -> Vec<std::path::PathBuf> {
949    let mut files = Vec::new();
950
951    // Check debian/source/lintian-overrides
952    let source_overrides = base_path.join("debian/source/lintian-overrides");
953    if source_overrides.exists() {
954        files.push(source_overrides);
955    }
956
957    // Check debian/*.lintian-overrides
958    let debian_dir = base_path.join("debian");
959    if debian_dir.exists() && debian_dir.is_dir() {
960        if let Ok(entries) = std::fs::read_dir(&debian_dir) {
961            for entry in entries.flatten() {
962                if let Some(filename) = entry.file_name().to_str() {
963                    if filename.ends_with(".lintian-overrides") {
964                        files.push(entry.path());
965                    }
966                }
967            }
968        }
969    }
970
971    files
972}
973
974/// Iterate over all lintian override lines in a debian directory
975pub fn iter_overrides(base_path: &std::path::Path) -> impl Iterator<Item = OverrideLine> {
976    let files = find_override_files(base_path);
977
978    files
979        .into_iter()
980        .flat_map(|override_file| {
981            let content = std::fs::read_to_string(&override_file).ok()?;
982            let parsed = LintianOverrides::parse(&content);
983            let overrides = parsed.ok().ok()?;
984            Some(overrides.lines().collect::<Vec<_>>())
985        })
986        .flatten()
987}
988
989#[cfg(test)]
990mod tests {
991    use super::*;
992
993    #[test]
994    fn test_info_matches_exact() {
995        assert!(info_matches("foo", "foo"));
996        assert!(!info_matches("foo", "bar"));
997    }
998
999    #[test]
1000    fn test_info_matches_wildcard_simple() {
1001        assert!(info_matches("*", "anything"));
1002        assert!(info_matches("*", ""));
1003        assert!(info_matches("**", "anything"));
1004    }
1005
1006    #[test]
1007    fn test_info_matches_wildcard_prefix() {
1008        assert!(info_matches("*.js", "file.js"));
1009        assert!(info_matches("*.js", "path/to/file.js"));
1010        assert!(!info_matches("*.js", "file.css"));
1011    }
1012
1013    #[test]
1014    fn test_info_matches_wildcard_suffix() {
1015        assert!(info_matches("debian/*", "debian/control"));
1016        assert!(info_matches("debian/*", "debian/rules"));
1017        assert!(!info_matches("debian/*", "other/file"));
1018    }
1019
1020    #[test]
1021    fn test_info_matches_wildcard_middle() {
1022        assert!(info_matches(
1023            "[debian/copyright:*]",
1024            "[debian/copyright:31]"
1025        ));
1026        assert!(info_matches(
1027            "[debian/copyright:*]",
1028            "[debian/copyright:100]"
1029        ));
1030        assert!(!info_matches("[debian/copyright:*]", "[debian/rules:31]"));
1031        assert!(!info_matches("[debian/copyright:*]", "debian/copyright:31"));
1032    }
1033
1034    #[test]
1035    fn test_info_matches_multiple_wildcards() {
1036        assert!(info_matches("*.html.*.js", "foo.html.bar.js"));
1037        assert!(info_matches("*.html.*.js", "foo.html.baz.qux.js"));
1038        assert!(!info_matches("*.html.*.js", "foo.css.bar.js"));
1039    }
1040
1041    #[test]
1042    fn test_info_matches_wildcard_empty_parts() {
1043        assert!(info_matches("foo**bar", "foobar"));
1044        assert!(info_matches("foo**bar", "fooxyzbar"));
1045    }
1046
1047    #[test]
1048    fn test_parse_simple_override() {
1049        let text = "some-tag\n";
1050        let parsed = LintianOverrides::parse(text);
1051        assert!(parsed.errors().is_empty());
1052
1053        let overrides = parsed.ok().unwrap();
1054        let lines: Vec<_> = overrides.lines().collect();
1055
1056        assert_eq!(lines.len(), 1);
1057        assert_eq!(lines[0].tag().unwrap().text(), "some-tag");
1058        assert_eq!(lines[0].info(), None);
1059    }
1060
1061    #[test]
1062    fn test_parse_override_with_info() {
1063        let text = "some-tag some extra info\n";
1064        let parsed = LintianOverrides::parse(text);
1065        assert!(parsed.errors().is_empty());
1066
1067        let overrides = parsed.ok().unwrap();
1068        let lines: Vec<_> = overrides.lines().collect();
1069
1070        assert_eq!(lines.len(), 1);
1071        assert_eq!(lines[0].tag().unwrap().text(), "some-tag");
1072        assert_eq!(lines[0].info(), Some("some extra info".to_string()));
1073    }
1074
1075    #[test]
1076    fn test_parse_package_override() {
1077        let text = "package-name: some-tag\n";
1078        let parsed = LintianOverrides::parse(text);
1079        assert!(parsed.errors().is_empty());
1080
1081        let overrides = parsed.ok().unwrap();
1082        let lines: Vec<_> = overrides.lines().collect();
1083
1084        assert_eq!(lines.len(), 1);
1085        assert_eq!(lines[0].tag().unwrap().text(), "some-tag");
1086        assert_eq!(
1087            lines[0].package_spec().unwrap().package_name().unwrap(),
1088            "package-name"
1089        );
1090    }
1091
1092    #[test]
1093    fn test_parse_comment() {
1094        let text = "# This is a comment\nsome-tag\n";
1095        let parsed = LintianOverrides::parse(text);
1096        assert!(parsed.errors().is_empty());
1097
1098        let overrides = parsed.ok().unwrap();
1099        let lines: Vec<_> = overrides.lines().collect();
1100
1101        assert_eq!(lines.len(), 2);
1102        assert!(lines[0].is_comment());
1103        assert_eq!(lines[1].tag().unwrap().text(), "some-tag");
1104    }
1105
1106    #[test]
1107    fn test_roundtrip() {
1108        let text = "# Comment\npackage: some-tag info\nanother-tag\n";
1109        let parsed = LintianOverrides::parse(text);
1110        assert!(parsed.errors().is_empty());
1111
1112        let overrides = parsed.ok().unwrap();
1113        assert_eq!(overrides.text(), text);
1114    }
1115
1116    #[test]
1117    fn test_builder() {
1118        let mut builder = LintianOverridesBuilder::new();
1119        builder.add_comment("# Test comment");
1120        builder.add_override(Some("mypackage"), "some-tag", Some("with info"));
1121        builder.add_override(None, "another-tag", None);
1122        let overrides = builder.finish();
1123
1124        let text = overrides.text();
1125        assert!(text.contains("# Test comment"));
1126        assert!(text.contains("mypackage: some-tag with info"));
1127        assert!(text.contains("another-tag"));
1128    }
1129
1130    #[test]
1131    fn test_parse_info_with_colon() {
1132        // Test that info fields containing colons are parsed correctly
1133        // This was a bug where "X-Python-Version: >= 2.5" would be misparsed
1134        let text = "ancient-python-version-field X-Python-Version: >= 2.5\n";
1135        let parsed = LintianOverrides::parse(text);
1136        assert!(parsed.errors().is_empty());
1137
1138        let overrides = parsed.ok().unwrap();
1139        let lines: Vec<_> = overrides.lines().collect();
1140
1141        assert_eq!(lines.len(), 1);
1142        assert_eq!(
1143            lines[0].tag().unwrap().text(),
1144            "ancient-python-version-field"
1145        );
1146        assert_eq!(
1147            lines[0].info(),
1148            Some("X-Python-Version: >= 2.5".to_string())
1149        );
1150        assert_eq!(lines[0].package_spec(), None);
1151    }
1152
1153    #[test]
1154    fn test_parse_source_prefix_with_info_containing_colon() {
1155        // Test parsing with explicit "source:" prefix and info containing colon
1156        let text = "source: ancient-python-version-field X-Python-Version: >= 2.5\n";
1157        let parsed = LintianOverrides::parse(text);
1158        assert!(parsed.errors().is_empty());
1159
1160        let overrides = parsed.ok().unwrap();
1161        let lines: Vec<_> = overrides.lines().collect();
1162
1163        assert_eq!(lines.len(), 1);
1164        assert_eq!(
1165            lines[0].tag().unwrap().text(),
1166            "ancient-python-version-field"
1167        );
1168        assert_eq!(
1169            lines[0].info(),
1170            Some("X-Python-Version: >= 2.5".to_string())
1171        );
1172        assert_eq!(
1173            lines[0].package_spec().unwrap().package_name().unwrap(),
1174            "source"
1175        );
1176    }
1177
1178    #[test]
1179    fn test_parse_two_word_non_package_spec() {
1180        // Test that two words before a colon that don't match package spec pattern
1181        // are not treated as a package spec
1182        let text = "some-tag field-name: value\n";
1183        let parsed = LintianOverrides::parse(text);
1184        assert!(parsed.errors().is_empty());
1185
1186        let overrides = parsed.ok().unwrap();
1187        let lines: Vec<_> = overrides.lines().collect();
1188
1189        assert_eq!(lines.len(), 1);
1190        assert_eq!(lines[0].tag().unwrap().text(), "some-tag");
1191        assert_eq!(lines[0].info(), Some("field-name: value".to_string()));
1192        assert_eq!(lines[0].package_spec(), None);
1193    }
1194
1195    #[test]
1196    fn test_filter_overrides_preserves_newlines() {
1197        let text = "# Comment\ntag1\ntag2\ntag3\n";
1198        let parsed = LintianOverrides::parse(text);
1199        let overrides = parsed.ok().unwrap();
1200
1201        // Filter out tag2
1202        let filtered = filter_overrides(&overrides, |line| {
1203            if let Some(tag) = line.tag() {
1204                tag.text() != "tag2"
1205            } else {
1206                true // Keep comments and empty lines
1207            }
1208        });
1209
1210        let result = filtered.to_string();
1211        let expected = "# Comment\ntag1\ntag3\n";
1212
1213        assert_eq!(
1214            result, expected,
1215            "Newlines should be preserved after filtering"
1216        );
1217    }
1218
1219    #[test]
1220    fn test_rename_tags_preserves_structure() {
1221        let text = "# keep this comment\npkg source: old-tag some info\nother-tag\n";
1222        let parsed = LintianOverrides::parse(text);
1223        let overrides = parsed.ok().unwrap();
1224        let renamed = rename_tags(&overrides, |tag| match tag {
1225            "old-tag" => Some("new-tag".to_string()),
1226            _ => None,
1227        });
1228        assert_eq!(
1229            renamed.text(),
1230            "# keep this comment\npkg source: new-tag some info\nother-tag\n"
1231        );
1232    }
1233
1234    #[test]
1235    fn test_rename_tags_no_match_no_change() {
1236        let text = "tag1\ntag2\n";
1237        let parsed = LintianOverrides::parse(text);
1238        let overrides = parsed.ok().unwrap();
1239        let renamed = rename_tags(&overrides, |_| None);
1240        assert_eq!(renamed.text(), text);
1241    }
1242
1243    #[test]
1244    fn test_filter_overrides_with_info() {
1245        let text = "pkg source: tag1\npkg source: tag2 info\npkg source: tag3\n";
1246        let parsed = LintianOverrides::parse(text);
1247        let overrides = parsed.ok().unwrap();
1248
1249        // Filter out tag2
1250        let filtered = filter_overrides(&overrides, |line| {
1251            if let Some(tag) = line.tag() {
1252                tag.text() != "tag2"
1253            } else {
1254                true
1255            }
1256        });
1257
1258        let result = filtered.to_string();
1259        let expected = "pkg source: tag1\npkg source: tag3\n";
1260
1261        assert_eq!(
1262            result, expected,
1263            "Newlines should be preserved with package specs and info"
1264        );
1265    }
1266
1267    #[test]
1268    fn test_parse_round_trip_without_trailing_newline() {
1269        // Regression: the parser used to append a NEWLINE token for every
1270        // line including non-newline-terminated trailers, so the round-trip
1271        // gained an extra '\n'.
1272        for input in [
1273            "",
1274            "\x0b",
1275            "tag info",
1276            "tag info\n",
1277            "tag info\ntag2 info\n",
1278            "tag info\ntag2 info",
1279            // Tag alone, with trailing whitespace that must round-trip.
1280            " \x12  ",
1281            "tag  ",
1282            "tag\t\t",
1283            // Package-spec text with unusual interior whitespace that the
1284            // previous hard-coded " " between words used to mangle.
1285            "0]\x0b:",
1286            "foo\tsource:",
1287            "binary  foo:",
1288        ] {
1289            let parsed = LintianOverrides::parse(input).tree();
1290            assert_eq!(parsed.text(), input, "round-trip differs for {:?}", input);
1291        }
1292    }
1293
1294    #[test]
1295    fn test_parse_arch_list() {
1296        let text = "foo [amd64]: some-tag\n";
1297        let parsed = LintianOverrides::parse(text);
1298        assert!(parsed.errors().is_empty());
1299
1300        let overrides = parsed.ok().unwrap();
1301        let lines: Vec<_> = overrides.lines().collect();
1302
1303        assert_eq!(lines.len(), 1);
1304        assert_eq!(lines[0].tag().unwrap().text(), "some-tag");
1305        assert_eq!(
1306            lines[0].package_spec().unwrap().package_name().unwrap(),
1307            "foo"
1308        );
1309        assert_eq!(lines[0].package_spec().unwrap().arch_list(), vec!["amd64"]);
1310    }
1311
1312    #[test]
1313    fn test_parse_arch_list_multiple() {
1314        let text = "foo [amd64 arm64]: some-tag\n";
1315        let parsed = LintianOverrides::parse(text);
1316        assert!(parsed.errors().is_empty());
1317
1318        let overrides = parsed.ok().unwrap();
1319        let lines: Vec<_> = overrides.lines().collect();
1320
1321        assert_eq!(lines.len(), 1);
1322        assert_eq!(
1323            lines[0].package_spec().unwrap().arch_list(),
1324            vec!["amd64", "arm64"]
1325        );
1326    }
1327
1328    #[test]
1329    fn test_parse_arch_list_negation() {
1330        let text = "foo [!amd64]: some-tag\n";
1331        let parsed = LintianOverrides::parse(text);
1332        assert!(parsed.errors().is_empty());
1333
1334        let overrides = parsed.ok().unwrap();
1335        let lines: Vec<_> = overrides.lines().collect();
1336
1337        assert_eq!(lines.len(), 1);
1338        // The '!' is part of the ARCH token text.
1339        assert_eq!(lines[0].package_spec().unwrap().arch_list(), vec!["!amd64"]);
1340    }
1341
1342    #[test]
1343    fn test_parse_arch_list_with_type() {
1344        let text = "foo [amd64] binary: some-tag\n";
1345        let parsed = LintianOverrides::parse(text);
1346        assert!(parsed.errors().is_empty());
1347
1348        let overrides = parsed.ok().unwrap();
1349        let lines: Vec<_> = overrides.lines().collect();
1350
1351        assert_eq!(lines.len(), 1);
1352        assert_eq!(
1353            lines[0].package_spec().unwrap().package_name().unwrap(),
1354            "foo"
1355        );
1356        assert_eq!(lines[0].package_spec().unwrap().arch_list(), vec!["amd64"]);
1357        assert_eq!(
1358            lines[0].package_spec().unwrap().package_type().unwrap(),
1359            "binary"
1360        );
1361    }
1362
1363    #[test]
1364    fn test_parse_arch_list_only() {
1365        // No package name, just arch list.
1366        let text = "[linux-any]: some-tag\n";
1367        let parsed = LintianOverrides::parse(text);
1368        assert!(parsed.errors().is_empty());
1369
1370        let overrides = parsed.ok().unwrap();
1371        let lines: Vec<_> = overrides.lines().collect();
1372
1373        assert_eq!(lines.len(), 1);
1374        assert_eq!(lines[0].tag().unwrap().text(), "some-tag");
1375        assert_eq!(
1376            lines[0].package_spec().unwrap().arch_list(),
1377            vec!["linux-any"]
1378        );
1379        assert_eq!(lines[0].package_spec().unwrap().package_name(), None);
1380    }
1381
1382    #[test]
1383    fn test_parse_arch_list_no_arch() {
1384        // A line without an arch list returns an empty vec.
1385        let text = "foo: some-tag\n";
1386        let parsed = LintianOverrides::parse(text);
1387        assert!(parsed.errors().is_empty());
1388
1389        let overrides = parsed.ok().unwrap();
1390        let lines: Vec<_> = overrides.lines().collect();
1391
1392        assert_eq!(
1393            lines[0].package_spec().unwrap().arch_list(),
1394            vec![] as Vec<String>
1395        );
1396    }
1397
1398    #[test]
1399    fn test_parse_arch_list_roundtrip() {
1400        // Arch list must survive a round-trip unchanged.
1401        for input in [
1402            "foo [amd64]: some-tag\n",
1403            "foo [amd64 arm64]: some-tag\n",
1404            "foo [!amd64]: some-tag\n",
1405            "foo [amd64] binary: some-tag\n",
1406            "[linux-any]: some-tag\n",
1407        ] {
1408            let parsed = LintianOverrides::parse(input).tree();
1409            assert_eq!(parsed.text(), input, "round-trip differs for {:?}", input);
1410        }
1411    }
1412}