1use breezyshim::error::Error as BrzError;
3use breezyshim::tree::MutableTree;
4use std::borrow::Cow;
5use std::io::BufRead;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TemplateType {
11 M4,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct GeneratedFile {
18 pub template_path: Option<PathBuf>,
20
21 pub template_type: Option<TemplateType>,
23}
24
25impl std::fmt::Display for GeneratedFile {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 write!(f, "File is generated")?;
28 if let Some(template_path) = &self.template_path {
29 write!(f, " from {}", template_path.display())?;
30 }
31 Ok(())
32 }
33}
34
35impl std::error::Error for GeneratedFile {}
36
37#[derive(Clone, PartialEq, Eq)]
38pub struct FormattingUnpreservable {
40 original_contents: Option<Vec<u8>>,
42
43 rewritten_contents: Option<Vec<u8>>,
45}
46
47impl std::fmt::Debug for FormattingUnpreservable {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("FormattingUnpreservable")
50 .field(
51 "original_contents",
52 &self
53 .original_contents
54 .as_deref()
55 .map(|x| std::str::from_utf8(x)),
56 )
57 .field(
58 "rewritten_contents",
59 &self
60 .rewritten_contents
61 .as_deref()
62 .map(|x| std::str::from_utf8(x)),
63 )
64 .finish()
65 }
66}
67
68impl std::fmt::Display for FormattingUnpreservable {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 write!(f, "Unable to preserve formatting",)
71 }
72}
73
74impl std::error::Error for FormattingUnpreservable {}
75
76impl FormattingUnpreservable {
77 pub fn diff(&self) -> Vec<String> {
79 let original_lines = std::str::from_utf8(self.original_contents.as_deref().unwrap_or(b""))
80 .unwrap()
81 .split_inclusive('\n')
82 .collect::<Vec<_>>();
83 let rewritten_lines =
84 std::str::from_utf8(self.rewritten_contents.as_deref().unwrap_or(b""))
85 .unwrap()
86 .split_inclusive('\n')
87 .collect::<Vec<_>>();
88
89 difflib::unified_diff(
90 original_lines.as_slice(),
91 rewritten_lines.as_slice(),
92 "original",
93 "rewritten",
94 "",
95 "",
96 3,
97 )
98 }
99}
100
101fn check_preserve_formatting(
108 rewritten_text: Option<&[u8]>,
109 text: Option<&[u8]>,
110 allow_reformatting: bool,
111) -> Result<(), FormattingUnpreservable> {
112 if allow_reformatting {
113 return Ok(());
114 }
115 if rewritten_text == text {
116 return Ok(());
117 }
118 Err(FormattingUnpreservable {
119 original_contents: text.map(|x| x.to_vec()),
120 rewritten_contents: rewritten_text.map(|x| x.to_vec()),
121 })
122}
123
124pub const DO_NOT_EDIT_SCAN_LINES: usize = 20;
126
127fn check_generated_contents(bufread: &mut dyn BufRead) -> Result<(), GeneratedFile> {
128 for l in bufread.lines().take(DO_NOT_EDIT_SCAN_LINES) {
129 let l = if let Ok(l) = l { l } else { continue };
130 if l.contains("DO NOT EDIT")
131 || l.contains("Do not edit!")
132 || l.contains("This file is autogenerated")
133 {
134 return Err(GeneratedFile {
135 template_path: None,
136 template_type: None,
137 });
138 }
139 }
140 Ok(())
141}
142
143pub const GENERATED_EXTENSIONS: &[&str] = &["in", "m4", "stub"];
145
146fn check_template_exists(path: &std::path::Path) -> Option<(PathBuf, Option<TemplateType>)> {
147 for ext in GENERATED_EXTENSIONS {
148 let template_path = path.with_extension(ext);
149 if template_path.exists() {
150 return Some((
151 template_path,
152 match ext {
153 &"m4" => Some(TemplateType::M4),
154 _ => None,
155 },
156 ));
157 }
158 }
159 None
160}
161
162fn tree_check_template_exists(
163 tree: &dyn MutableTree,
164 path: &std::path::Path,
165) -> Option<(PathBuf, Option<TemplateType>)> {
166 for ext in GENERATED_EXTENSIONS {
167 let template_path = path.with_extension(ext);
168 if tree.has_filename(&template_path) {
169 return Some((
170 template_path,
171 match ext {
172 &"m4" => Some(TemplateType::M4),
173 _ => None,
174 },
175 ));
176 }
177 }
178 None
179}
180
181pub fn check_generated_file(path: &std::path::Path) -> Result<(), GeneratedFile> {
189 if let Some((template_path, template_type)) = check_template_exists(path) {
190 return Err(GeneratedFile {
191 template_path: Some(template_path),
192 template_type,
193 });
194 }
195
196 match std::fs::File::open(path) {
197 Ok(f) => {
198 let mut buf = std::io::BufReader::new(f);
199 check_generated_contents(&mut buf)?;
200 }
201 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
202 Err(e) => panic!("Error reading file: {}", e),
203 }
204 Ok(())
205}
206
207pub fn tree_check_generated_file(
215 tree: &dyn MutableTree,
216 path: &std::path::Path,
217) -> Result<(), GeneratedFile> {
218 if let Some((template_path, template_type)) = tree_check_template_exists(tree, path) {
219 return Err(GeneratedFile {
220 template_path: Some(template_path),
221 template_type,
222 });
223 }
224
225 match tree.get_file(path) {
226 Ok(f) => {
227 let mut buf = std::io::BufReader::new(f);
228 check_generated_contents(&mut buf)?;
229 }
230 Err(BrzError::NoSuchFile(..)) => {}
231 Err(e) => panic!("Error reading file: {}", e),
232 }
233 Ok(())
234}
235
236#[derive(Debug)]
237pub enum EditorError {
239 GeneratedFile(PathBuf, GeneratedFile),
241
242 TemplateError(PathBuf, String),
244
245 FormattingUnpreservable(PathBuf, FormattingUnpreservable),
247
248 IoError(std::io::Error),
250
251 BrzError(BrzError),
253}
254
255impl From<BrzError> for EditorError {
256 fn from(e: BrzError) -> Self {
257 EditorError::BrzError(e)
258 }
259}
260
261impl std::fmt::Display for EditorError {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 match self {
264 EditorError::GeneratedFile(p, _e) => {
265 write!(f, "File {} is generated from another file", p.display())
266 }
267 EditorError::FormattingUnpreservable(p, _e) => {
268 write!(f, "Unable to preserve formatting in {}", p.display())
269 }
270 EditorError::IoError(e) => write!(f, "I/O error: {}", e),
271 EditorError::BrzError(e) => write!(f, "Breezy error: {}", e),
272 EditorError::TemplateError(p, e) => {
273 write!(f, "Error in template {}: {}", p.display(), e)
274 }
275 }
276 }
277}
278
279impl std::error::Error for EditorError {}
280
281impl From<std::io::Error> for EditorError {
282 fn from(e: std::io::Error) -> Self {
283 EditorError::IoError(e)
284 }
285}
286
287#[cfg(feature = "merge3")]
288fn update_with_merge3(
290 original_contents: &[u8],
291 rewritten_contents: &[u8],
292 updated_contents: &[u8],
293) -> Option<Vec<u8>> {
294 let rewritten_lines = rewritten_contents
295 .split_inclusive(|&x| x == b'\n')
296 .collect::<Vec<_>>();
297 let original_lines = original_contents
298 .split_inclusive(|&x| x == b'\n')
299 .collect::<Vec<_>>();
300 let updated_lines = updated_contents
301 .split_inclusive(|&x| x == b'\n')
302 .collect::<Vec<_>>();
303 let m3 = merge3::Merge3::new(
304 rewritten_lines.as_slice(),
305 original_lines.as_slice(),
306 updated_lines.as_slice(),
307 );
308 if m3
309 .merge_regions()
310 .iter()
311 .any(|x| matches!(x, merge3::MergeRegion::Conflict { .. }))
312 {
313 return None;
314 }
315 Some(
316 m3.merge_lines(false, &merge3::StandardMarkers::default())
317 .concat(),
318 )
319}
320
321fn reformat_file<'a>(
333 original_contents: Option<&'a [u8]>,
334 rewritten_contents: Option<&'a [u8]>,
335 updated_contents: Option<&'a [u8]>,
336 allow_reformatting: bool,
337) -> Result<(Option<Cow<'a, [u8]>>, bool), FormattingUnpreservable> {
338 if updated_contents == rewritten_contents || updated_contents == original_contents {
339 return Ok((updated_contents.map(Cow::Borrowed), false));
340 }
341 #[allow(unused_mut)]
342 let mut updated_contents = updated_contents.map(std::borrow::Cow::Borrowed);
343 match check_preserve_formatting(rewritten_contents, original_contents, allow_reformatting) {
344 Ok(()) => {}
345 Err(e) => {
346 if rewritten_contents.is_none()
347 || original_contents.is_none()
348 || updated_contents.is_none()
349 {
350 return Err(e);
351 }
352 #[cfg(feature = "merge3")]
353 {
354 log::debug!("Unable to preserve formatting; falling back to merge3");
356 updated_contents = Some(std::borrow::Cow::Owned(
357 if let Some(lines) = update_with_merge3(
358 original_contents.unwrap(),
359 rewritten_contents.unwrap(),
360 updated_contents.unwrap().as_ref(),
361 ) {
362 lines
363 } else {
364 return Err(e);
365 },
366 ));
367 }
368 #[cfg(not(feature = "merge3"))]
369 {
370 log::debug!("Unable to preserve formatting; merge3 feature not enabled");
371 return Err(e);
372 }
373 }
374 }
375
376 Ok((updated_contents, true))
377}
378
379pub fn edit_formatted_file(
393 path: &std::path::Path,
394 original_contents: Option<&[u8]>,
395 rewritten_contents: Option<&[u8]>,
396 updated_contents: Option<&[u8]>,
397 allow_generated: bool,
398 allow_reformatting: bool,
399) -> Result<bool, EditorError> {
400 if original_contents == updated_contents {
401 return Ok(false);
402 }
403 let (updated_contents, changed) = reformat_file(
404 original_contents,
405 rewritten_contents,
406 updated_contents,
407 allow_reformatting,
408 )
409 .map_err(|e| EditorError::FormattingUnpreservable(path.to_path_buf(), e))?;
410 if changed && !allow_generated {
411 check_generated_file(path)
412 .map_err(|e| EditorError::GeneratedFile(path.to_path_buf(), e))?;
413 }
414
415 if changed {
416 if let Some(updated_contents) = updated_contents {
417 std::fs::write(path, updated_contents)?;
418 } else {
419 std::fs::remove_file(path)?;
420 }
421 }
422 Ok(changed)
423}
424
425pub fn tree_edit_formatted_file(
440 tree: &dyn MutableTree,
441 path: &std::path::Path,
442 original_contents: Option<&[u8]>,
443 rewritten_contents: Option<&[u8]>,
444 updated_contents: Option<&[u8]>,
445 allow_generated: bool,
446 allow_reformatting: bool,
447) -> Result<bool, EditorError> {
448 assert!(path.is_relative());
449 if original_contents == updated_contents {
450 return Ok(false);
451 }
452 if !allow_generated {
453 tree_check_generated_file(tree, path)
454 .map_err(|e| EditorError::GeneratedFile(path.to_path_buf(), e))?;
455 }
456
457 let (updated_contents, changed) = reformat_file(
458 original_contents,
459 rewritten_contents,
460 updated_contents,
461 allow_reformatting,
462 )
463 .map_err(|e| EditorError::FormattingUnpreservable(path.to_path_buf(), e))?;
464 if changed {
465 if let Some(updated_contents) = updated_contents {
466 tree.put_file_bytes_non_atomic(path, updated_contents.as_ref())?;
467 tree.add(&[path])?;
468 } else if tree.has_filename(path) {
469 tree.remove(&[path])?;
470 }
471 }
472 Ok(changed)
473}
474
475pub trait Marshallable {
477 fn from_bytes(content: &[u8]) -> Self;
479
480 fn empty() -> Self;
482
483 fn to_bytes(&self) -> Option<Vec<u8>>;
485}
486
487pub trait Editor<P: Marshallable>:
489 std::ops::Deref<Target = P> + std::ops::DerefMut<Target = P>
490{
491 fn orig_content(&self) -> Option<&[u8]>;
493
494 fn updated_content(&self) -> Option<Vec<u8>>;
496
497 fn rewritten_content(&self) -> Option<&[u8]>;
499
500 fn has_changed(&self) -> bool {
502 self.updated_content().as_deref() != self.rewritten_content()
503 }
504
505 fn is_generated(&self) -> bool;
507
508 fn commit(&self) -> Result<Vec<std::path::PathBuf>, EditorError>;
513}
514
515pub trait MutableTreeEdit {
517 fn edit_file<P: Marshallable>(
519 &self,
520 path: &std::path::Path,
521 allow_generated: bool,
522 allow_reformatting: bool,
523 ) -> Result<TreeEditor<'_, P>, EditorError>;
524}
525
526impl<T: MutableTree> MutableTreeEdit for T {
527 fn edit_file<P: Marshallable>(
528 &self,
529 path: &std::path::Path,
530 allow_generated: bool,
531 allow_reformatting: bool,
532 ) -> Result<TreeEditor<'_, P>, EditorError> {
533 TreeEditor::new(self, path, allow_generated, allow_reformatting)
534 }
535}
536
537pub struct TreeEditor<'a, P: Marshallable> {
539 tree: &'a dyn MutableTree,
540 path: PathBuf,
541 orig_content: Option<Vec<u8>>,
542 rewritten_content: Option<Vec<u8>>,
543 allow_generated: bool,
544 allow_reformatting: bool,
545 parsed: Option<P>,
546}
547
548impl<P: Marshallable> std::ops::Deref for TreeEditor<'_, P> {
549 type Target = P;
550
551 fn deref(&self) -> &Self::Target {
552 self.parsed.as_ref().unwrap()
553 }
554}
555
556impl<P: Marshallable> std::ops::DerefMut for TreeEditor<'_, P> {
557 fn deref_mut(&mut self) -> &mut Self::Target {
558 self.parsed.as_mut().unwrap()
559 }
560}
561
562impl<'a, P: Marshallable> TreeEditor<'a, P> {
563 pub fn from_env(
565 tree: &'a dyn MutableTree,
566 path: &std::path::Path,
567 allow_generated: bool,
568 allow_reformatting: Option<bool>,
569 ) -> Result<Self, EditorError> {
570 let allow_reformatting = allow_reformatting.unwrap_or_else(|| {
571 std::env::var("REFORMATTING").unwrap_or("disallow".to_string()) == "allow"
572 });
573
574 Self::new(tree, path, allow_generated, allow_reformatting)
575 }
576
577 fn read(&mut self) -> Result<(), EditorError> {
579 self.orig_content = match self.tree.get_file_text(&self.path) {
580 Ok(c) => Some(c),
581 Err(BrzError::NoSuchFile(..)) => None,
582 Err(e) => return Err(e.into()),
583 };
584 self.parsed = match self.orig_content.as_deref() {
585 Some(content) => Some(P::from_bytes(content)),
586 None => Some(P::empty()),
587 };
588 self.rewritten_content = self.parsed.as_ref().unwrap().to_bytes();
589 Ok(())
590 }
591
592 pub fn new(
594 tree: &'a dyn MutableTree,
595 path: &std::path::Path,
596 allow_generated: bool,
597 allow_reformatting: bool,
598 ) -> Result<Self, EditorError> {
599 assert!(path.is_relative());
600 let mut ret = Self {
601 tree,
602 path: path.to_path_buf(),
603 orig_content: None,
604 rewritten_content: None,
605 allow_generated,
606 allow_reformatting,
607 parsed: None,
608 };
609 ret.read()?;
610 Ok(ret)
611 }
612}
613
614impl<P: Marshallable> Editor<P> for TreeEditor<'_, P> {
615 fn orig_content(&self) -> Option<&[u8]> {
616 self.orig_content.as_deref()
617 }
618
619 fn updated_content(&self) -> Option<Vec<u8>> {
620 self.parsed.as_ref().unwrap().to_bytes()
621 }
622
623 fn rewritten_content(&self) -> Option<&[u8]> {
624 self.rewritten_content.as_deref()
625 }
626
627 fn commit(&self) -> Result<Vec<std::path::PathBuf>, EditorError> {
628 let updated_content = self.updated_content();
629
630 let changed = tree_edit_formatted_file(
631 self.tree,
632 &self.path,
633 self.orig_content.as_deref(),
634 self.rewritten_content.as_deref(),
635 updated_content.as_deref(),
636 self.allow_generated,
637 self.allow_reformatting,
638 )?;
639 if changed {
640 Ok(vec![self.path.clone()])
641 } else {
642 Ok(vec![])
643 }
644 }
645
646 fn is_generated(&self) -> bool {
647 tree_check_generated_file(self.tree, &self.path).is_ok() || {
648 let mut buf =
649 std::io::BufReader::new(std::io::Cursor::new(self.orig_content().unwrap()));
650 check_generated_contents(&mut buf).is_err()
651 }
652 }
653}
654
655pub struct FsEditor<P: Marshallable> {
657 path: PathBuf,
658 orig_content: Option<Vec<u8>>,
659 rewritten_content: Option<Vec<u8>>,
660 allow_generated: bool,
661 allow_reformatting: bool,
662 parsed: Option<P>,
663}
664
665impl<M: Marshallable> std::ops::Deref for FsEditor<M> {
666 type Target = M;
667
668 fn deref(&self) -> &Self::Target {
669 self.parsed.as_ref().unwrap()
670 }
671}
672
673impl<M: Marshallable> std::ops::DerefMut for FsEditor<M> {
674 fn deref_mut(&mut self) -> &mut Self::Target {
675 self.parsed.as_mut().unwrap()
676 }
677}
678
679impl<P: Marshallable> FsEditor<P> {
680 pub fn from_env(
682 path: &std::path::Path,
683 allow_generated: bool,
684 allow_reformatting: Option<bool>,
685 ) -> Result<Self, EditorError> {
686 let allow_reformatting = allow_reformatting.unwrap_or_else(|| {
687 std::env::var("REFORMATTING").unwrap_or("disallow".to_string()) == "allow"
688 });
689
690 Self::new(path, allow_generated, allow_reformatting)
691 }
692
693 fn read(&mut self) -> Result<(), EditorError> {
695 self.orig_content = match std::fs::read(&self.path) {
696 Ok(c) => Some(c),
697 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
698 Err(e) => return Err(e.into()),
699 };
700 self.parsed = match self.orig_content.as_deref() {
701 Some(content) => Some(P::from_bytes(content)),
702 None => Some(P::empty()),
703 };
704 self.rewritten_content = self.parsed.as_ref().unwrap().to_bytes();
705 Ok(())
706 }
707
708 pub fn new(
710 path: &std::path::Path,
711 allow_generated: bool,
712 allow_reformatting: bool,
713 ) -> Result<Self, EditorError> {
714 let mut ret = Self {
715 path: path.to_path_buf(),
716 orig_content: None,
717 rewritten_content: None,
718 allow_generated,
719 allow_reformatting,
720 parsed: None,
721 };
722 ret.read()?;
723 Ok(ret)
724 }
725}
726
727impl<P: Marshallable> Editor<P> for FsEditor<P> {
728 fn orig_content(&self) -> Option<&[u8]> {
729 self.orig_content.as_deref()
730 }
731
732 fn updated_content(&self) -> Option<Vec<u8>> {
733 self.parsed.as_ref().unwrap().to_bytes()
734 }
735
736 fn rewritten_content(&self) -> Option<&[u8]> {
737 self.rewritten_content.as_deref()
738 }
739
740 fn is_generated(&self) -> bool {
741 check_template_exists(&self.path).is_some() || {
742 let mut buf =
743 std::io::BufReader::new(std::io::Cursor::new(self.orig_content().unwrap()));
744 check_generated_contents(&mut buf).is_err()
745 }
746 }
747
748 fn commit(&self) -> Result<Vec<std::path::PathBuf>, EditorError> {
749 let updated_content = self.updated_content();
750
751 let changed = edit_formatted_file(
752 &self.path,
753 self.orig_content.as_deref(),
754 self.rewritten_content.as_deref(),
755 updated_content.as_deref(),
756 self.allow_generated,
757 self.allow_reformatting,
758 )?;
759 if changed {
760 Ok(vec![self.path.clone()])
761 } else {
762 Ok(vec![])
763 }
764 }
765}
766
767impl Marshallable for debian_control::Control {
768 fn from_bytes(content: &[u8]) -> Self {
769 debian_control::Control::read_relaxed(std::io::Cursor::new(content))
770 .unwrap()
771 .0
772 }
773
774 fn empty() -> Self {
775 debian_control::Control::new()
776 }
777
778 fn to_bytes(&self) -> Option<Vec<u8>> {
779 self.source()?;
780 Some(self.to_string().into_bytes())
781 }
782}
783
784impl Marshallable for debian_control::lossy::Control {
785 fn from_bytes(content: &[u8]) -> Self {
786 use std::str::FromStr;
787 debian_control::lossy::Control::from_str(std::str::from_utf8(content).unwrap()).unwrap()
788 }
789
790 fn empty() -> Self {
791 debian_control::lossy::Control::new()
792 }
793
794 fn to_bytes(&self) -> Option<Vec<u8>> {
795 Some(self.to_string().into_bytes())
796 }
797}
798
799impl Marshallable for debian_changelog::ChangeLog {
800 fn from_bytes(content: &[u8]) -> Self {
801 debian_changelog::ChangeLog::read_relaxed(std::io::Cursor::new(content)).unwrap()
802 }
803
804 fn empty() -> Self {
805 debian_changelog::ChangeLog::new()
806 }
807
808 fn to_bytes(&self) -> Option<Vec<u8>> {
809 Some(self.to_string().into_bytes())
810 }
811}
812
813impl Marshallable for debian_copyright::lossless::Copyright {
814 fn from_bytes(content: &[u8]) -> Self {
815 debian_copyright::lossless::Copyright::from_str_relaxed(
816 std::str::from_utf8(content).unwrap(),
817 )
818 .unwrap()
819 .0
820 }
821
822 fn empty() -> Self {
823 debian_copyright::lossless::Copyright::new()
824 }
825
826 fn to_bytes(&self) -> Option<Vec<u8>> {
827 Some(self.to_string().into_bytes())
828 }
829}
830
831impl Marshallable for makefile_lossless::Makefile {
832 fn from_bytes(content: &[u8]) -> Self {
833 makefile_lossless::Makefile::read_relaxed(std::io::Cursor::new(content)).unwrap()
834 }
835
836 fn empty() -> Self {
837 makefile_lossless::Makefile::new()
838 }
839
840 fn to_bytes(&self) -> Option<Vec<u8>> {
841 Some(self.to_string().into_bytes())
842 }
843}
844
845impl Marshallable for deb822_lossless::Deb822 {
846 fn from_bytes(content: &[u8]) -> Self {
847 deb822_lossless::Deb822::read_relaxed(std::io::Cursor::new(content))
848 .unwrap()
849 .0
850 }
851
852 fn empty() -> Self {
853 deb822_lossless::Deb822::new()
854 }
855
856 fn to_bytes(&self) -> Option<Vec<u8>> {
857 Some(self.to_string().into_bytes())
858 }
859}
860
861impl Marshallable for crate::maintscripts::Maintscript {
862 fn from_bytes(content: &[u8]) -> Self {
863 use std::str::FromStr;
864 let content = std::str::from_utf8(content).unwrap();
865 crate::maintscripts::Maintscript::from_str(content).unwrap()
866 }
867
868 fn empty() -> Self {
869 crate::maintscripts::Maintscript::new()
870 }
871
872 fn to_bytes(&self) -> Option<Vec<u8>> {
873 if self.is_empty() {
874 None
875 } else {
876 Some(self.to_string().into_bytes())
877 }
878 }
879}
880
881#[cfg(test)]
882mod tests {
883 use super::*;
884 #[test]
885 fn test_formatting_same() {
886 assert_eq!(
887 Ok(()),
888 check_preserve_formatting(Some(b"FOO "), Some(b"FOO "), false)
889 );
890 }
891
892 #[test]
893 fn test_formatting_different() {
894 assert_eq!(
895 Err(FormattingUnpreservable {
896 original_contents: Some("FOO \n".as_bytes().to_vec()),
897 rewritten_contents: Some("FOO \n".as_bytes().to_vec()),
898 }),
899 check_preserve_formatting(Some(b"FOO \n"), Some(b"FOO \n"), false)
900 );
901 }
902
903 #[test]
904 fn test_diff() {
905 let e = FormattingUnpreservable {
906 original_contents: Some(b"FOO X\n".to_vec()),
907 rewritten_contents: Some(b"FOO X\n".to_vec()),
908 };
909 assert_eq!(
910 e.diff(),
911 vec![
912 "--- original\t\n",
913 "+++ rewritten\t\n",
914 "@@ -1 +1 @@\n",
915 "-FOO X\n",
916 "+FOO X\n",
917 ]
918 );
919 }
920
921 #[test]
922 fn test_reformatting_allowed() {
923 assert_eq!(
924 Ok(()),
925 check_preserve_formatting(Some(b"FOO "), Some(b"FOO "), true)
926 );
927 }
928
929 #[test]
930 fn test_generated_control_file() {
931 let td = tempfile::tempdir().unwrap();
932 std::fs::create_dir(td.path().join("debian")).unwrap();
933 std::fs::write(td.path().join("debian/control.in"), "Source: blah\n").unwrap();
934 assert_eq!(
935 Err(GeneratedFile {
936 template_path: Some(td.path().join("debian/control.in")),
937 template_type: None,
938 }),
939 check_generated_file(&td.path().join("debian/control"))
940 );
941 }
942
943 #[test]
944 fn test_generated_file_missing() {
945 let td = tempfile::tempdir().unwrap();
946 std::fs::create_dir(td.path().join("debian")).unwrap();
947 assert_eq!(
948 Ok(()),
949 check_generated_file(&td.path().join("debian/control"))
950 );
951 }
952
953 #[test]
954 fn test_do_not_edit() {
955 let td = tempfile::tempdir().unwrap();
956 std::fs::create_dir(td.path().join("debian")).unwrap();
957 std::fs::write(
958 td.path().join("debian/control"),
959 "# DO NOT EDIT\nSource: blah\n",
960 )
961 .unwrap();
962 assert_eq!(
963 Err(GeneratedFile {
964 template_path: None,
965 template_type: None,
966 }),
967 check_generated_file(&td.path().join("debian/control"))
968 );
969 }
970
971 #[test]
972 fn test_do_not_edit_after_header() {
973 let td = tempfile::tempdir().unwrap();
975 std::fs::create_dir(td.path().join("debian")).unwrap();
976 std::fs::write(
977 td.path().join("debian/control"),
978 "\n".repeat(50) + "# DO NOT EDIT\nSource: blah\n",
979 )
980 .unwrap();
981 assert_eq!(
982 Ok(()),
983 check_generated_file(&td.path().join("debian/control"))
984 );
985 }
986
987 #[test]
988 fn test_unchanged() {
989 let td = tempfile::tempdir().unwrap();
990 std::fs::write(td.path().join("a"), "some content\n").unwrap();
991 assert!(!edit_formatted_file(
992 &td.path().join("a"),
993 Some("some content\n".as_bytes()),
994 Some("some content reformatted\n".as_bytes()),
995 Some("some content\n".as_bytes()),
996 false,
997 false
998 )
999 .unwrap());
1000 assert!(!edit_formatted_file(
1001 &td.path().join("a"),
1002 Some("some content\n".as_bytes()),
1003 Some("some content\n".as_bytes()),
1004 Some("some content\n".as_bytes()),
1005 false,
1006 false
1007 )
1008 .unwrap());
1009 assert!(!edit_formatted_file(
1010 &td.path().join("a"),
1011 Some("some content\n".as_bytes()),
1012 Some("some content reformatted\n".as_bytes()),
1013 Some("some content reformatted\n".as_bytes()),
1014 false,
1015 false
1016 )
1017 .unwrap());
1018 }
1019
1020 #[test]
1021 fn test_changed() {
1022 let td = tempfile::tempdir().unwrap();
1023 std::fs::write(td.path().join("a"), "some content\n").unwrap();
1024 assert!(edit_formatted_file(
1025 &td.path().join("a"),
1026 Some("some content\n".as_bytes()),
1027 Some("some content\n".as_bytes()),
1028 Some("new content\n".as_bytes()),
1029 false,
1030 false
1031 )
1032 .unwrap());
1033 assert_eq!(
1034 "new content\n",
1035 std::fs::read_to_string(td.path().join("a")).unwrap()
1036 );
1037 }
1038
1039 #[test]
1040 fn test_unformattable() {
1041 let td = tempfile::tempdir().unwrap();
1042 assert!(matches!(
1043 edit_formatted_file(
1044 &td.path().join("a"),
1045 Some(b"some content\n"),
1046 Some(b"reformatted content\n"),
1047 Some(b"new content\n"),
1048 false,
1049 false
1050 )
1051 .unwrap_err(),
1052 EditorError::FormattingUnpreservable(_, FormattingUnpreservable { .. })
1053 ));
1054 }
1055
1056 struct TestMarshall {
1057 data: Option<usize>,
1058 }
1059
1060 impl TestMarshall {
1061 fn get_data(&self) -> Option<usize> {
1062 self.data
1063 }
1064
1065 fn unset_data(&mut self) {
1066 self.data = None;
1067 }
1068
1069 fn inc_data(&mut self) {
1070 match &mut self.data {
1071 Some(x) => *x += 1,
1072 None => self.data = Some(1),
1073 }
1074 }
1075 }
1076
1077 impl Marshallable for TestMarshall {
1078 fn from_bytes(content: &[u8]) -> Self {
1079 let data = std::str::from_utf8(content).unwrap().parse().unwrap();
1080 Self { data: Some(data) }
1081 }
1082
1083 fn empty() -> Self {
1084 Self { data: None }
1085 }
1086
1087 fn to_bytes(&self) -> Option<Vec<u8>> {
1088 self.data.map(|x| x.to_string().into_bytes())
1089 }
1090 }
1091
1092 #[test]
1093 fn test_edit_create_file() {
1094 let td = tempfile::tempdir().unwrap();
1095
1096 let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1097 assert!(!editor.has_changed());
1098 editor.inc_data();
1099 assert_eq!(editor.get_data(), Some(1));
1100 assert!(editor.has_changed());
1101 assert_eq!(editor.commit().unwrap(), vec![td.path().join("a")]);
1102 assert_eq!(editor.get_data(), Some(1));
1103
1104 assert_eq!("1", std::fs::read_to_string(td.path().join("a")).unwrap());
1105 }
1106
1107 #[test]
1108 fn test_edit_create_no_changes() {
1109 let td = tempfile::tempdir().unwrap();
1110
1111 let editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1112 assert!(!editor.has_changed());
1113 assert_eq!(editor.commit().unwrap(), Vec::<std::path::PathBuf>::new());
1114 assert_eq!(editor.get_data(), None);
1115 assert!(!td.path().join("a").exists());
1116 }
1117
1118 #[test]
1119 fn test_edit_change() {
1120 let td = tempfile::tempdir().unwrap();
1121 std::fs::write(td.path().join("a"), "1").unwrap();
1122
1123 let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1124 assert!(!editor.has_changed());
1125 editor.inc_data();
1126 assert_eq!(editor.get_data(), Some(2));
1127 assert!(editor.has_changed());
1128 assert_eq!(editor.commit().unwrap(), vec![td.path().join("a")]);
1129 assert_eq!(editor.get_data(), Some(2));
1130
1131 assert_eq!("2", std::fs::read_to_string(td.path().join("a")).unwrap());
1132 }
1133
1134 #[test]
1135 fn test_edit_delete() {
1136 let td = tempfile::tempdir().unwrap();
1137 std::fs::write(td.path().join("a"), "1").unwrap();
1138
1139 let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1140 assert!(!editor.has_changed());
1141 editor.unset_data();
1142 assert_eq!(editor.get_data(), None);
1143 assert!(editor.has_changed());
1144 assert_eq!(editor.commit().unwrap(), vec![td.path().join("a")]);
1145 assert_eq!(editor.get_data(), None);
1146
1147 assert!(!td.path().join("a").exists());
1148 }
1149
1150 #[test]
1151 fn test_tree_editor_edit() {
1152 use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
1153 let tempdir = tempfile::tempdir().unwrap();
1154
1155 let tree =
1156 create_standalone_workingtree(tempdir.path(), &ControlDirFormat::default()).unwrap();
1157
1158 let mut editor = tree
1159 .edit_file::<TestMarshall>(std::path::Path::new("a"), false, false)
1160 .unwrap();
1161
1162 assert!(!editor.has_changed());
1163 editor.inc_data();
1164 assert_eq!(editor.get_data(), Some(1));
1165 assert!(editor.has_changed());
1166 assert_eq!(editor.commit().unwrap(), vec![std::path::Path::new("a")]);
1167
1168 assert_eq!(
1169 "1",
1170 std::fs::read_to_string(tempdir.path().join("a")).unwrap()
1171 );
1172 }
1173
1174 #[test]
1175 fn test_tree_edit_control() {
1176 use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
1177 let tempdir = tempfile::tempdir().unwrap();
1178
1179 let tree =
1180 create_standalone_workingtree(tempdir.path(), &ControlDirFormat::default()).unwrap();
1181
1182 tree.mkdir(std::path::Path::new("debian")).unwrap();
1183
1184 let mut editor = tree
1185 .edit_file::<debian_control::Control>(
1186 std::path::Path::new("debian/control"),
1187 false,
1188 false,
1189 )
1190 .unwrap();
1191
1192 assert!(!editor.has_changed());
1193 let mut source = editor.add_source("blah");
1194 source.set_homepage(&"https://example.com".parse().unwrap());
1195 assert!(editor.has_changed());
1196 assert_eq!(
1197 editor.commit().unwrap(),
1198 vec![std::path::Path::new("debian/control")]
1199 );
1200
1201 assert_eq!(
1202 "Source: blah\nHomepage: https://example.com/\n",
1203 std::fs::read_to_string(tempdir.path().join("debian/control")).unwrap()
1204 );
1205 }
1206
1207 #[test]
1208 fn test_merge3() {
1209 let td = tempfile::tempdir().unwrap();
1210 std::fs::create_dir(td.path().join("debian")).unwrap();
1211 std::fs::write(
1212 td.path().join("debian/control"),
1213 r#"Source: blah
1214Testsuite: autopkgtest
1215
1216Package: blah
1217Description: Some description
1218 And there are more lines
1219 And more lines
1220# A comment
1221Multi-Arch: foreign
1222"#,
1223 )
1224 .unwrap();
1225
1226 let mut editor = super::FsEditor::<debian_control::lossy::Control>::new(
1227 &td.path().join("debian/control"),
1228 false,
1229 false,
1230 )
1231 .unwrap();
1232 editor.source.homepage = Some("https://example.com".parse().unwrap());
1233
1234 #[cfg(feature = "merge3")]
1235 {
1236 editor.commit().unwrap();
1237 assert_eq!(
1238 r#"Source: blah
1239Homepage: https://example.com/
1240Testsuite: autopkgtest
1241
1242Package: blah
1243Multi-Arch: foreign
1244Description: Some description
1245 And there are more lines
1246 And more lines
1247"#,
1248 editor.to_string()
1249 );
1250 }
1251 #[cfg(not(feature = "merge3"))]
1252 {
1253 let result = editor.commit();
1254 let updated_content =
1255 std::fs::read_to_string(td.path().join("debian/control")).unwrap();
1256 assert!(result.is_err(), "{:?}", updated_content);
1257 assert!(
1258 matches!(
1259 result.as_ref().unwrap_err(),
1260 super::EditorError::FormattingUnpreservable(_, _)
1261 ),
1262 "{:?} {:?}",
1263 result,
1264 updated_content
1265 );
1266 assert_eq!(
1267 r#"Source: blah
1268Testsuite: autopkgtest
1269
1270Package: blah
1271Description: Some description
1272 And there are more lines
1273 And more lines
1274# A comment
1275Multi-Arch: foreign
1276"#,
1277 updated_content
1278 );
1279 }
1280 }
1281
1282 #[test]
1283 fn test_reformat_file_preserved() {
1284 let (updated_content, changed) = reformat_file(
1285 Some(b"original\n"),
1286 Some(b"original\n"),
1287 Some(b"updated\n"),
1288 false,
1289 )
1290 .unwrap();
1291 assert_eq!(updated_content, Some(Cow::Borrowed(&b"updated\n"[..])));
1292 assert!(changed);
1293 }
1294
1295 #[test]
1296 fn test_reformat_file_not_preserved_allowed() {
1297 let (updated_content, changed) = reformat_file(
1298 Some(b"original\n#comment\n"),
1299 Some(b"original\n"),
1300 Some(b"updated\n"),
1301 true,
1302 )
1303 .unwrap();
1304 assert_eq!(updated_content, Some(Cow::Borrowed(&b"updated\n"[..])));
1305 assert!(changed);
1306 }
1307
1308 #[test]
1309 fn test_reformat_file_not_preserved_not_allowed() {
1310 let err = reformat_file(
1311 Some(b"original\n#comment\n"),
1312 Some(b"original\n"),
1313 Some(b"updated\n"),
1314 false,
1315 )
1316 .unwrap_err();
1317 assert!(matches!(err, FormattingUnpreservable { .. }));
1318 }
1319
1320 #[test]
1321 fn test_reformat_file_not_preserved_merge3() {
1322 let r = reformat_file(
1323 Some(b"original\noriginal 2\noriginal 3\n#comment\noriginal 4\n"),
1324 Some(b"original\noriginal 2\noriginal 3\noriginal 4\n"),
1325 Some(b"updated\noriginal 2\noriginal 3\n#comment\noriginal 4\n"),
1326 false,
1327 );
1328 #[cfg(feature = "merge3")]
1329 {
1330 let (updated_content, changed) = r.unwrap();
1331 let updated = std::str::from_utf8(updated_content.as_ref().unwrap().as_ref()).unwrap();
1332 assert_eq!(
1333 updated,
1334 "updated\noriginal 2\noriginal 3\n#comment\noriginal 4\n"
1335 );
1336 assert!(changed);
1337 }
1338 #[cfg(not(feature = "merge3"))]
1339 {
1340 assert!(matches!(r.unwrap_err(), FormattingUnpreservable { .. }));
1341 }
1342 }
1343
1344 #[test]
1345 fn test_edit_formatted_file_preservable() {
1346 let td = tempfile::tempdir().unwrap();
1347 std::fs::write(td.path().join("a"), "some content\n").unwrap();
1348 assert!(edit_formatted_file(
1349 &td.path().join("a"),
1350 Some("some content\n".as_bytes()),
1351 Some("some content\n".as_bytes()),
1352 Some("new content\n".as_bytes()),
1353 false,
1354 false
1355 )
1356 .unwrap());
1357 assert_eq!(
1358 "new content\n",
1359 std::fs::read_to_string(td.path().join("a")).unwrap()
1360 );
1361 }
1362
1363 #[test]
1364 fn test_edit_formatted_file_not_preservable() {
1365 let td = tempfile::tempdir().unwrap();
1366 std::fs::write(td.path().join("a"), "some content\n#extra\n").unwrap();
1367 assert!(matches!(
1368 edit_formatted_file(
1369 &td.path().join("a"),
1370 Some("some content\n#extra\n".as_bytes()),
1371 Some("some content\n".as_bytes()),
1372 Some("new content\n".as_bytes()),
1373 false,
1374 false
1375 )
1376 .unwrap_err(),
1377 EditorError::FormattingUnpreservable(_, FormattingUnpreservable { .. })
1378 ));
1379
1380 assert_eq!(
1381 "some content\n#extra\n",
1382 std::fs::read_to_string(td.path().join("a")).unwrap()
1383 );
1384 }
1385
1386 #[test]
1387 fn test_edit_formatted_file_not_preservable_allowed() {
1388 let td = tempfile::tempdir().unwrap();
1389 std::fs::write(td.path().join("a"), "some content\n").unwrap();
1390 assert!(edit_formatted_file(
1391 &td.path().join("a"),
1392 Some("some content\n#extra\n".as_bytes()),
1393 Some("some content\n".as_bytes()),
1394 Some("new content\n".as_bytes()),
1395 false,
1396 true
1397 )
1398 .is_ok());
1399
1400 assert_eq!(
1401 "new content\n",
1402 std::fs::read_to_string(td.path().join("a")).unwrap()
1403 );
1404 }
1405}