debian_analyzer/
editor.rs

1//! Editing files
2use 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)]
9/// The type of template used to generate a file.
10pub enum TemplateType {
11    /// M4 template
12    M4,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16/// Error indicating that a file is generated from another file.
17pub struct GeneratedFile {
18    /// The path to the template file
19    pub template_path: Option<PathBuf>,
20
21    /// The type of template
22    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)]
38/// Error indicating that formatting could not be preserved.
39pub struct FormattingUnpreservable {
40    /// The original contents of the file
41    original_contents: Option<Vec<u8>>,
42
43    /// The contents rewritten with our parser/serializer
44    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    /// Get a unified diff of the original and rewritten contents.
78    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
101/// Check that formatting can be preserved.
102///
103/// # Arguments
104/// * `rewritten_text` - The rewritten file contents
105/// * `text` - The original file contents
106/// * `allow_reformatting` - Whether to allow reformatting
107fn 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
124/// Number of lines to scan for generated file indicators.
125pub 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
143/// List of extensions for template files.
144pub 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
181/// Check if a file is generated from another file.
182///
183/// # Arguments
184/// * `path` - Path to the file to check
185///
186/// # Errors
187/// * `GeneratedFile` - when a generated file is found
188pub 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
207/// Check if a file is generated from another file.
208///
209/// # Arguments
210/// * `path` - Path to the file to check
211///
212/// # Errors
213/// * `GeneratedFile` - when a generated file is found
214pub 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)]
237/// Error that can occur when editing a file.
238pub enum EditorError {
239    /// One of the files is generated from another file, and we were unable to edit it.
240    GeneratedFile(PathBuf, GeneratedFile),
241
242    /// Error in a template file
243    TemplateError(PathBuf, String),
244
245    /// Unable to preserve formatting in a file.
246    FormattingUnpreservable(PathBuf, FormattingUnpreservable),
247
248    /// I/O error
249    IoError(std::io::Error),
250
251    /// Breezy error
252    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")]
288/// Update a file with a three-way merge.
289fn 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
321/// Reformat a file.
322///
323/// # Arguments
324/// * `original_contents` - The original contents of the file
325/// * `rewritten_contents` - The contents rewritten with our parser/serializer
326/// * `updated_contents` - Updated contents rewritten with our parser/serializer after changes were
327///  made
328///  * `allow_reformatting` - Whether to allow reformatting of the file
329///
330///  # Returns
331///  A tuple with the new contents and a boolean indicating whether the file was changed`
332fn 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                // Run three way merge
355                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
379/// Edit a formatted file.
380///
381/// # Arguments
382/// * `path` - Path to the file
383/// * `original_contents` - The original contents of the file
384/// * `rewritten_contents` - The contents rewritten with our parser/serializer
385/// * `updated_contents` - Updated contents rewritten with our parser/serializer after changes were
386///   made
387/// * `allow_generated` - Do not raise `GeneratedFile` when encountering a generated file
388/// * `allow_reformatting` - Whether to allow reformatting of the file
389///
390/// # Returns
391/// `true` if the file was changed, `false` otherwise
392pub 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
425/// Edit a formatted file in a tree.
426///
427/// # Arguments
428/// * `tree` - The tree to edit
429/// * `path` - Path to the file
430/// * `original_contents` - The original contents of the file
431/// * `rewritten_contents` - The contents rewritten with our parser/serializer
432/// * `updated_contents` - Updated contents rewritten with our parser/serializer after changes were
433///   made
434/// * `allow_generated` - Do not raise `GeneratedFile` when encountering a generated file
435/// * `allow_reformatting` - Whether to allow reformatting of the file
436///
437/// # Returns
438/// `true` if the file was changed, `false` otherwise
439pub 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
475/// A trait for types that can be edited
476pub trait Marshallable {
477    /// Parse the contents of a file
478    fn from_bytes(content: &[u8]) -> Self;
479
480    /// Create an empty instance
481    fn empty() -> Self;
482
483    /// Serialize the contents of a file
484    fn to_bytes(&self) -> Option<Vec<u8>>;
485}
486
487/// An editor for a file
488pub trait Editor<P: Marshallable>:
489    std::ops::Deref<Target = P> + std::ops::DerefMut<Target = P>
490{
491    /// The original content, if any - without reformatting
492    fn orig_content(&self) -> Option<&[u8]>;
493
494    /// The updated content, if any
495    fn updated_content(&self) -> Option<Vec<u8>>;
496
497    /// The original content, but rewritten with our parser/serializer
498    fn rewritten_content(&self) -> Option<&[u8]>;
499
500    /// Whether the file has changed
501    fn has_changed(&self) -> bool {
502        self.updated_content().as_deref() != self.rewritten_content()
503    }
504
505    /// Check if the file is generated
506    fn is_generated(&self) -> bool;
507
508    /// Commit the changes
509    ///
510    /// # Returns
511    /// A list of paths that were changed
512    fn commit(&self) -> Result<Vec<std::path::PathBuf>, EditorError>;
513}
514
515/// Allow calling .edit_file("debian/control") on a tree
516pub trait MutableTreeEdit {
517    /// Edit a file in a tree
518    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
537/// An editor for a file in a breezy tree
538pub 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    /// Create a new editor, with preferences being read from the environment
564    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    /// Read the file contents and parse them
578    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    /// Create a new editor
593    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
655/// An editor for a file
656pub 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    /// Create a new editor, with preferences being read from the environment
681    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    /// Read the file contents and parse them
694    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    /// Create a new editor
709    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        // check_generated_file() only checks the first 20 lines.
974        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}