Skip to content

Commit

Permalink
fix: Always sort StringList (#226)
Browse files Browse the repository at this point in the history
We now use a sorted set for storing paths and tags used in snapshots.
This ensures that independent from how they are saved, identical sets
are always grouped together.

closes rustic-rs/rustic#1158
  • Loading branch information
aawsome authored Jun 10, 2024
1 parent f1f73db commit c47ba6c
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 26 deletions.
2 changes: 1 addition & 1 deletion crates/core/src/commands/repair/snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl Default for RepairSnapshotsOptions {
Self {
delete: true,
suffix: ".repaired".to_string(),
tag: vec![StringList(vec!["repaired".to_string()])],
tag: vec![StringList(BTreeSet::from(["repaired".to_string()]))],
}
}
}
Expand Down
69 changes: 44 additions & 25 deletions crates/core/src/repofile/snapshotfile.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
cmp::Ordering,
collections::BTreeMap,
collections::{BTreeMap, BTreeSet},
fmt::{self, Display},
path::{Path, PathBuf},
str::FromStr,
Expand Down Expand Up @@ -678,7 +678,6 @@ impl SnapshotFile {
pub fn add_tags(&mut self, tag_lists: Vec<StringList>) -> bool {
let old_tags = self.tags.clone();
self.tags.add_all(tag_lists);
self.tags.sort();

old_tags != self.tags
}
Expand All @@ -695,7 +694,6 @@ impl SnapshotFile {
pub fn set_tags(&mut self, tag_lists: Vec<StringList>) -> bool {
let old_tags = std::mem::take(&mut self.tags);
self.tags.add_all(tag_lists);
self.tags.sort();

old_tags != self.tags
}
Expand Down Expand Up @@ -946,7 +944,7 @@ impl SnapshotGroup {

/// `StringList` is a rustic-internal list of Strings. It is used within [`SnapshotFile`]
#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct StringList(pub(crate) Vec<String>);
pub struct StringList(pub(crate) BTreeSet<String>);

impl FromStr for StringList {
type Err = RusticError;
Expand All @@ -957,7 +955,7 @@ impl FromStr for StringList {

impl Display for StringList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.join(","))?;
write!(f, "{}", self.0.iter().join(","))?;
Ok(())
}
}
Expand All @@ -970,7 +968,7 @@ impl StringList {
/// * `s` - The String to check
#[must_use]
pub fn contains(&self, s: &str) -> bool {
self.0.iter().any(|m| m == s)
self.0.contains(s)
}

/// Returns whether a [`StringList`] contains all Strings of another [`StringList`].
Expand All @@ -980,7 +978,7 @@ impl StringList {
/// * `sl` - The [`StringList`] to check
#[must_use]
pub fn contains_all(&self, sl: &Self) -> bool {
sl.0.iter().all(|s| self.contains(s))
self.0.is_subset(&sl.0)
}

/// Returns whether a [`StringList`] matches a list of [`StringList`]s,
Expand All @@ -1000,20 +998,16 @@ impl StringList {
///
/// * `s` - The String to add
pub fn add(&mut self, s: String) {
if !self.contains(&s) {
self.0.push(s);
}
_ = self.0.insert(s);
}

/// Add all Strings from another [`StringList`] to this [`StringList`].
///
/// # Arguments
///
/// * `sl` - The [`StringList`] to add
pub fn add_list(&mut self, sl: Self) {
for s in sl.0 {
self.add(s);
}
pub fn add_list(&mut self, mut sl: Self) {
self.0.append(&mut sl.0);
}

/// Add all Strings from all given [`StringList`]s to this [`StringList`].
Expand Down Expand Up @@ -1047,7 +1041,7 @@ impl StringList {
.ok_or_else(|| SnapshotFileErrorKind::NonUnicodePath(p.as_ref().to_path_buf()))?
.to_string())
})
.collect::<RusticResult<Vec<_>>>()?;
.collect::<RusticResult<BTreeSet<_>>>()?;
Ok(())
}

Expand All @@ -1057,33 +1051,33 @@ impl StringList {
///
/// * `string_lists` - The [`StringList`]s to remove
pub fn remove_all(&mut self, string_lists: &[Self]) {
self.0
.retain(|s| !string_lists.iter().any(|sl| sl.contains(s)));
for sl in string_lists {
self.0 = &self.0 - &sl.0;
}
}

#[deprecated(note = "StringLists are now automatically sorted")]
/// Sort the Strings in the [`StringList`]
pub fn sort(&mut self) {
self.0.sort_unstable();
}
pub fn sort(&mut self) {}

/// Format this [`StringList`] using newlines
#[must_use]
pub fn formatln(&self) -> String {
self.0.join("\n")
self.0.iter().join("\n")
}

/// Turn this [`StringList`] into an Iterator
pub fn iter(&self) -> std::slice::Iter<'_, String> {
pub fn iter(&self) -> impl Iterator<Item = &String> {
self.0.iter()
}
}

impl<'str> IntoIterator for &'str StringList {
type Item = &'str String;
type IntoIter = std::slice::Iter<'str, String>;
type IntoIter = std::collections::btree_set::Iter<'str, String>;

fn into_iter(self) -> Self::IntoIter {
self.iter()
self.0.iter()
}
}

Expand Down Expand Up @@ -1208,6 +1202,7 @@ fn sanitize_dot(path: &Path) -> RusticResult<PathBuf> {
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;

use rstest::rstest;

Expand All @@ -1218,10 +1213,34 @@ mod tests {
#[case("test/", "test")]
#[case("./test", "test")]
#[case("./test/", "test")]
fn escape_cases(#[case] input: &str, #[case] expected: &str) {
fn sanitize_dot_cases(#[case] input: &str, #[case] expected: &str) {
let path = Path::new(input);
let expected = PathBuf::from(expected);

assert_eq!(expected, sanitize_dot(path).unwrap());
}

#[rstest]
#[case("abc", vec!["abc".to_string()])]
#[case("abc,def", vec!["abc".to_string(), "def".to_string()])]
#[case("abc,abc", vec!["abc".to_string()])]
fn test_set_tags(#[case] tag: &str, #[case] expected: Vec<String>) -> Result<()> {
let mut snap = SnapshotFile::from_options(&SnapshotOptions::default())?;
let tags = StringList::from_str(tag)?;
let expected = StringList(expected.into_iter().collect());
assert!(snap.set_tags(vec![tags]));
assert_eq!(snap.tags, expected);
Ok(())
}

#[test]
fn test_add_tags() -> Result<()> {
let tag = vec![StringList::from_str("abc")?];
let mut snap = SnapshotFile::from_options(&SnapshotOptions::default().tag(tag))?;
let tags = StringList::from_str("def,abc")?;
assert!(snap.add_tags(vec![tags]));
let expected = StringList::from_str("abc,def")?;
assert_eq!(snap.tags, expected);
Ok(())
}
}

0 comments on commit c47ba6c

Please sign in to comment.