Repository: bottlerocket-os/bottlerocket
File: sources/api/migration/migration-helpers/src/common_migrations.rs (Rust)

Code before:
pub struct AddPrefixSuffixMigration(pub Vec<PrefixSuffix>);

impl Migration for AddPrefixSuffixMigration {
    /// New versions must either have a default for the settings or generate them; we don't need to
    /// do anything.
    fn forward(&mut self, input: MigrationData) -> Result<MigrationData> {
        println!(
            "AddPrefixSuffixMigration({:?}) has no work to do on upgrade.",
            self.0
                .iter()
                .map(|ps| format!("{}*{}", ps.prefix, ps.suffix))
                .collect::<Vec<_>>()
        );
        Ok(input)
    }

    /// Older versions don't know about the settings; we remove them so that old versions don't see
    /// them and fail deserialization.
    fn backward(&mut self, mut input: MigrationData) -> Result<MigrationData> {
        let settings = input
            .data
            .keys()
            .filter(|k| {
                self.0
                    .iter()
                    .any(|pattern| k.starts_with(pattern.prefix) && k.ends_with(pattern.suffix))
            })
            .cloned()
            .collect::<Vec<_>>();
        for setting in settings {
            if let Some(data) = input.data.remove(&setting) {
                println!("Removed {setting}, which was set to '{data}'");
            }
        }
        Ok(input)
    }
}

#[cfg(test)]
mod test_add_prefix_suffix_migration {
    use super::{AddPrefixSuffixMigration, PrefixSuffix};
    use crate::{Migration, MigrationData};
    use maplit::hashmap;
    use std::collections::HashMap;

    #[test]
    fn single_entry() {
        let data = MigrationData {
            data: hashmap! {
                "keep.stuff.like.this".into() => 0.into(),
                "remove.stuff.like.this".into() => 0.into(),

Review comment (suggestion):
This seems kind of loose:
1. it will match "settings.host-containers.command", if someone has named their host container that
2. it would hypothetically match "settings.host-containers-plugins.command" if there were a different extension

So I'd recommend enforcing the segment rules - we want to match `<prefix>.<one or more chars>.<suffix>`.

Code after:
}
pub struct AddPrefixSuffixMigration(pub Vec<PrefixSuffix>);

impl Migration for AddPrefixSuffixMigration {
    /// New versions must either have a default for the settings or generate them; we don't need to
    /// do anything.
    fn forward(&mut self, input: MigrationData) -> Result<MigrationData> {
        println!(
            "AddPrefixSuffixMigration({:?}) has no work to do on upgrade.",
            self.0
                .iter()
                .map(|ps| format!("{}*{}", ps.prefix, ps.suffix))
                .collect::<Vec<_>>()
        );
        Ok(input)
    }

    /// Older versions don't know about the settings; we remove them so that old versions don't see
    /// them and fail deserialization.
    fn backward(&mut self, mut input: MigrationData) -> Result<MigrationData> {
        let mut compiled_patterns = Vec::new();
        for pattern in &self.0 {
            let regex_pattern = format!(
                r"^{}\.(.+)\.{}$",
                regex::escape(pattern.prefix),
                regex::escape(pattern.suffix)
            );
            let regex =
                Regex::new(&regex_pattern).context(error::InvalidPrefixSuffixPatternSnafu {
                    prefix: pattern.prefix,
                    suffix: pattern.suffix,
                })?;
            compiled_patterns.push(regex);
        }

        let settings = input
            .data
            .keys()
            .filter(|k| compiled_patterns.iter().any(|regex| regex.is_match(k)))
            .cloned()
            .collect::<Vec<_>>();
        for setting in settings {
            if let Some(data) = input.data.remove(&setting) {
                println!("Removed {setting}, which was set to '{data}'");
            }
        }
        Ok(input)
    }
}

#[cfg(test)]