1use rowan::{GreenNode, GreenNodeBuilder};
2use std::fmt;
3
4pub fn info_matches(pattern: &str, value: &str) -> bool {
9 if pattern == value {
10 return true;
11 }
12
13 if !pattern.contains('*') {
15 return false;
16 }
17
18 let parts: Vec<&str> = pattern.split('*').collect();
20
21 if !parts[0].is_empty() && !value.starts_with(parts[0]) {
23 return false;
24 }
25
26 if !parts[parts.len() - 1].is_empty() && !value.ends_with(parts[parts.len() - 1]) {
28 return false;
29 }
30
31 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)]
50pub enum SyntaxKind {
52 WHITESPACE = 0,
54 COMMENT,
56 PACKAGE_NAME,
58 L_BRACKET,
60 R_BRACKET,
62 ARCH,
64 COLON,
66 PACKAGE_TYPE,
68 TAG,
70 INFO,
72 NEWLINE,
74 ROOT,
76 OVERRIDE_LINE,
78 PACKAGE_SPEC,
80 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#[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
109pub type SyntaxNode = rowan::SyntaxNode<Lang>;
111pub type SyntaxToken = rowan::SyntaxToken<Lang>;
113pub type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
115
116#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct Parse<T> {
127 green: GreenNode,
128 errors: Vec<String>,
129 _phantom: std::marker::PhantomData<T>,
130}
131
132unsafe 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 pub fn syntax(&self) -> SyntaxNode {
149 SyntaxNode::new_root(self.green.clone())
150 }
151
152 pub fn errors(&self) -> &[String] {
154 &self.errors
155 }
156
157 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 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
185pub trait AstNode: Clone {
187 fn cast(syntax: SyntaxNode) -> Option<Self>
189 where
190 Self: Sized;
191
192 fn syntax(&self) -> &SyntaxNode;
194}
195
196#[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 pub fn snapshot(&self) -> Self {
224 LintianOverrides {
225 syntax: SyntaxNode::new_root_mut(self.syntax.green().into_owned()),
226 }
227 }
228
229 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 pub fn parse(text: &str) -> Parse<Self> {
242 let (green, errors) = parse_lintian_overrides(text);
243 Parse::new(green, errors)
244 }
245
246 pub fn lines(&self) -> impl Iterator<Item = OverrideLine> + '_ {
248 self.syntax.children().filter_map(OverrideLine::cast)
249 }
250
251 pub fn text(&self) -> String {
253 self.syntax.text().to_string()
254 }
255
256 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#[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 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 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 pub fn package_spec(&self) -> Option<PackageSpec> {
305 self.syntax.children().find_map(PackageSpec::cast)
306 }
307
308 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 pub fn tag_range(&self) -> Option<rowan::TextRange> {
318 self.tag().map(|t| t.text_range())
319 }
320
321 pub fn text_range(&self) -> rowan::TextRange {
323 self.syntax.text_range()
324 }
325
326 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 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 pub fn package(&self) -> Option<String> {
365 self.package_spec()?.package_name()
366 }
367
368 pub fn text(&self) -> String {
370 self.syntax.text().to_string()
371 }
372
373 pub fn package_type(&self) -> Option<String> {
376 self.package_spec()?.package_type()
377 }
378
379 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 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 if let Some(pkg_spec) = self.package_spec() {
404 if let Some(pkg_name) = pkg_spec.package_name() {
406 if Some(pkg_name.as_str()) != issue_package {
407 return false;
408 }
409 }
410 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 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#[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 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 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 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
482fn 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 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 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 if trimmed_start.starts_with('#') {
519 builder.token(COMMENT.into(), trimmed_start);
520 builder.finish_node();
521 return;
522 }
523
524 if trimmed_start.is_empty() {
526 builder.finish_node();
527 return;
528 }
529
530 let mut current_start = 0;
532
533 let mut has_package_spec = false;
540 let mut colon_pos = 0;
541
542 if let Some(pos) = trimmed_start.find(':') {
543 let after_colon = &trimmed_start[pos + 1..];
545 if after_colon.is_empty() || after_colon.starts_with(char::is_whitespace) {
546 let before_colon = &trimmed_start[..pos];
548 if is_valid_package_spec(before_colon) {
549 has_package_spec = true;
551 colon_pos = pos;
552 }
553 }
554 }
555
556 if has_package_spec {
557 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 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 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
609fn 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 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 if rest.starts_with('[') {
626 match rest.find(']') {
627 Some(end) => rest = rest[end + 1..].trim_start(),
628 None => return false, }
630 }
631
632 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 rest.is_empty()
643}
644
645fn parse_package_spec_tokens(builder: &mut GreenNodeBuilder, spec: &str) {
647 let mut rest = spec;
648
649 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 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 if rest.starts_with('[') {
675 if let Some(end) = rest.find(']') {
676 builder.token(L_BRACKET.into(), "[");
677
678 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 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 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
719pub struct LintianOverridesBuilder<'a> {
721 builder: GreenNodeBuilder<'a>,
722}
723
724impl<'a> LintianOverridesBuilder<'a> {
725 pub fn new() -> Self {
727 let mut builder = GreenNodeBuilder::new();
728 builder.start_node(ROOT.into());
729 Self { builder }
730 }
731
732 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 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 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
786pub 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
802pub 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
830pub 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
897pub 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 if let Some((package, package_type, tag, info)) = transform(&line) {
910 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 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
947pub fn find_override_files(base_path: &std::path::Path) -> Vec<std::path::PathBuf> {
949 let mut files = Vec::new();
950
951 let source_overrides = base_path.join("debian/source/lintian-overrides");
953 if source_overrides.exists() {
954 files.push(source_overrides);
955 }
956
957 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
974pub 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 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 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 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 let filtered = filter_overrides(&overrides, |line| {
1203 if let Some(tag) = line.tag() {
1204 tag.text() != "tag2"
1205 } else {
1206 true }
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 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 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 " \x12 ",
1281 "tag ",
1282 "tag\t\t",
1283 "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 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 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 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 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}