diff --git a/libparsec/crates/client/src/config.rs b/libparsec/crates/client/src/config.rs index bf0bdbcf84c..310eb506652 100644 --- a/libparsec/crates/client/src/config.rs +++ b/libparsec/crates/client/src/config.rs @@ -46,7 +46,7 @@ pub struct ClientConfig { pub mountpoint_mount_strategy: MountpointMountStrategy, pub workspace_storage_cache_size: WorkspaceStorageCacheSize, /// The pattern used to filter out files that should not be synced with the server like temporary files. - pub prevent_sync_pattern: Regex, + pub prevent_sync_pattern: PreventSyncPattern, pub proxy: ProxyConfig, /// If `false`, nothing runs & react in the background, useful for tests /// or CLI where the client is started to only perform a single operation. diff --git a/libparsec/crates/client/src/workspace/history/populate_cache.rs b/libparsec/crates/client/src/workspace/history/populate_cache.rs index 92b87e51796..0b7f61caca4 100644 --- a/libparsec/crates/client/src/workspace/history/populate_cache.rs +++ b/libparsec/crates/client/src/workspace/history/populate_cache.rs @@ -63,7 +63,7 @@ pub(super) async fn populate_cache_from_server( LocalFileManifest::from_remote(manifest), ))), Ok(ChildManifest::Folder(manifest)) => Some(ArcLocalChildManifest::Folder(Arc::new( - LocalFolderManifest::from_remote(manifest, &Regex::empty()), + LocalFolderManifest::from_remote(manifest, &PreventSyncPattern::empty()), ))), // This is unexpected: we got an entry ID from a parent folder/workspace // manifest, but this ID points to nothing according to the server :/ diff --git a/libparsec/crates/client/src/workspace/merge.rs b/libparsec/crates/client/src/workspace/merge.rs index 4db24a8c552..eb447314bfd 100644 --- a/libparsec/crates/client/src/workspace/merge.rs +++ b/libparsec/crates/client/src/workspace/merge.rs @@ -241,7 +241,7 @@ pub(super) fn merge_local_file_manifest( pub(super) fn merge_local_folder_manifest( local_author: DeviceID, timestamp: DateTime, - prevent_sync_pattern: &Regex, + prevent_sync_pattern: &PreventSyncPattern, local: &LocalFolderManifest, remote: FolderManifest, ) -> MergeLocalFolderManifestOutcome { diff --git a/libparsec/crates/client/src/workspace/store/mod.rs b/libparsec/crates/client/src/workspace/store/mod.rs index 53164ae31e1..448c690356a 100644 --- a/libparsec/crates/client/src/workspace/store/mod.rs +++ b/libparsec/crates/client/src/workspace/store/mod.rs @@ -117,7 +117,7 @@ pub(super) struct WorkspaceStore { /// Given accessing `storage` requires exclusive access, it is better to have it /// under its own lock so that all cache hit operations can occur concurrently. storage: AsyncMutex>, - prevent_sync_pattern: Regex, + prevent_sync_pattern: PreventSyncPattern, } impl std::panic::UnwindSafe for WorkspaceStore {} @@ -130,7 +130,7 @@ impl WorkspaceStore { certificates_ops: Arc, cache_size: u64, realm_id: VlobID, - pattern: &Regex, + prevent_sync_pattern: &PreventSyncPattern, ) -> Result { // 1) Open the database @@ -179,7 +179,7 @@ impl WorkspaceStore { prevent_sync_pattern::ensure_prevent_sync_pattern_applied_to_wksp( &mut storage, device.clone(), - pattern, + prevent_sync_pattern, ) .await?; @@ -192,7 +192,7 @@ impl WorkspaceStore { certificates_ops, current_view_cache: Mutex::new(CurrentViewCache::new(Arc::new(root_manifest.into()))), storage: AsyncMutex::new(Some(storage)), - prevent_sync_pattern: pattern.clone(), + prevent_sync_pattern: prevent_sync_pattern.clone(), }) } diff --git a/libparsec/crates/client/src/workspace/store/prevent_sync_pattern.rs b/libparsec/crates/client/src/workspace/store/prevent_sync_pattern.rs index bcd90415fe5..572f7f74fe4 100644 --- a/libparsec/crates/client/src/workspace/store/prevent_sync_pattern.rs +++ b/libparsec/crates/client/src/workspace/store/prevent_sync_pattern.rs @@ -13,12 +13,12 @@ pub enum ApplyPreventSyncPatternError { pub(super) async fn ensure_prevent_sync_pattern_applied_to_wksp( storage: &mut WorkspaceStorage, device: Arc, - pattern: &Regex, + prevent_sync_pattern: &PreventSyncPattern, ) -> Result<(), ApplyPreventSyncPatternError> { const PAGE_SIZE: u32 = 1000; let fully_applied = storage - .set_prevent_sync_pattern(pattern) + .set_prevent_sync_pattern(prevent_sync_pattern) .await .map_err(ApplyPreventSyncPatternError::Internal)?; @@ -38,8 +38,8 @@ pub(super) async fn ensure_prevent_sync_pattern_applied_to_wksp( // Only the prevent sync pattern could be applied to a folder type manifest. // We assume that workspace manifest and folder manifest could both be deserialize as folder manifest. if let Some(folder) = decode_folder(encoded_manifest, &device.local_symkey)? { - let new_folder = - folder.apply_prevent_sync_pattern(pattern, device.time_provider.now()); + let new_folder = folder + .apply_prevent_sync_pattern(prevent_sync_pattern, device.time_provider.now()); if new_folder != folder { updated_manifest.push(UpdateManifestData { entry_id: new_folder.base.id, @@ -65,7 +65,7 @@ pub(super) async fn ensure_prevent_sync_pattern_applied_to_wksp( offset += PAGE_SIZE; } storage - .mark_prevent_sync_pattern_fully_applied(pattern) + .mark_prevent_sync_pattern_fully_applied(prevent_sync_pattern) .await .map_err(|e| ApplyPreventSyncPatternError::Internal(e.into()))?; Ok(()) diff --git a/libparsec/crates/client/tests/unit/certif/utils.rs b/libparsec/crates/client/tests/unit/certif/utils.rs index 268f4859f83..cb0835c80a1 100644 --- a/libparsec/crates/client/tests/unit/certif/utils.rs +++ b/libparsec/crates/client/tests/unit/certif/utils.rs @@ -22,7 +22,7 @@ pub(crate) async fn certificates_ops_factory( workspace_storage_cache_size: WorkspaceStorageCacheSize::Default, proxy: ProxyConfig::default(), with_monitors: false, - prevent_sync_pattern: Regex::empty(), + prevent_sync_pattern: PreventSyncPattern::empty(), }); let event_bus = EventBus::default(); let cmds = Arc::new( diff --git a/libparsec/crates/client/tests/unit/client/utils.rs b/libparsec/crates/client/tests/unit/client/utils.rs index 8770a22b28a..cab44272d7a 100644 --- a/libparsec/crates/client/tests/unit/client/utils.rs +++ b/libparsec/crates/client/tests/unit/client/utils.rs @@ -22,7 +22,7 @@ pub(crate) async fn client_factory( workspace_storage_cache_size: WorkspaceStorageCacheSize::Default, proxy: ProxyConfig::default(), with_monitors: false, - prevent_sync_pattern: Regex::empty(), + prevent_sync_pattern: PreventSyncPattern::empty(), }); Client::start(config, event_bus, device).await.unwrap() } diff --git a/libparsec/crates/client/tests/unit/client/with_monitors.rs b/libparsec/crates/client/tests/unit/client/with_monitors.rs index cb50e82abb8..2ba05f81445 100644 --- a/libparsec/crates/client/tests/unit/client/with_monitors.rs +++ b/libparsec/crates/client/tests/unit/client/with_monitors.rs @@ -23,7 +23,7 @@ async fn multi_devices(env: &TestbedEnv) { workspace_storage_cache_size: WorkspaceStorageCacheSize::Default, proxy: libparsec_client_connection::ProxyConfig::default(), with_monitors: true, - prevent_sync_pattern: Regex::empty(), + prevent_sync_pattern: PreventSyncPattern::empty(), }); let alice1_event_bus = EventBus::default(); let alice2_event_bus = EventBus::default(); @@ -139,7 +139,7 @@ async fn sharing(env: &TestbedEnv) { workspace_storage_cache_size: WorkspaceStorageCacheSize::Default, proxy: libparsec_client_connection::ProxyConfig::default(), with_monitors: true, - prevent_sync_pattern: Regex::empty(), + prevent_sync_pattern: PreventSyncPattern::empty(), }); let alice_event_bus = EventBus::default(); let bob_event_bus = EventBus::default(); diff --git a/libparsec/crates/client/tests/unit/invite/bootstrap_organization.rs b/libparsec/crates/client/tests/unit/invite/bootstrap_organization.rs index fc03cb02025..6f1a4fcae45 100644 --- a/libparsec/crates/client/tests/unit/invite/bootstrap_organization.rs +++ b/libparsec/crates/client/tests/unit/invite/bootstrap_organization.rs @@ -27,7 +27,7 @@ fn make_config(env: &TestbedEnv) -> Arc { proxy: ProxyConfig::default(), mountpoint_mount_strategy: MountpointMountStrategy::Disabled, with_monitors: false, - prevent_sync_pattern: Regex::from_regex_str(r"\.tmp$").unwrap(), + prevent_sync_pattern: PreventSyncPattern::from_regex(r"\.tmp$").unwrap(), }) } diff --git a/libparsec/crates/client/tests/unit/invite/claimer.rs b/libparsec/crates/client/tests/unit/invite/claimer.rs index df13cb633db..947e3b07ac4 100644 --- a/libparsec/crates/client/tests/unit/invite/claimer.rs +++ b/libparsec/crates/client/tests/unit/invite/claimer.rs @@ -30,7 +30,7 @@ async fn claimer(tmp_path: TmpPath, env: &TestbedEnv) { proxy: ProxyConfig::default(), mountpoint_mount_strategy: MountpointMountStrategy::Disabled, with_monitors: false, - prevent_sync_pattern: Regex::from_regex_str(r"\.tmp$").unwrap(), + prevent_sync_pattern: PreventSyncPattern::from_regex(r"\.tmp$").unwrap(), }); let alice = env.local_device("alice@dev1"); diff --git a/libparsec/crates/client/tests/unit/user/utils.rs b/libparsec/crates/client/tests/unit/user/utils.rs index 6b984b80570..e28cb4d916d 100644 --- a/libparsec/crates/client/tests/unit/user/utils.rs +++ b/libparsec/crates/client/tests/unit/user/utils.rs @@ -19,7 +19,7 @@ pub(crate) async fn user_ops_factory(env: &TestbedEnv, device: &Arc workspace_storage_cache_size: WorkspaceStorageCacheSize::Default, proxy: ProxyConfig::default(), with_monitors: false, - prevent_sync_pattern: Regex::empty(), + prevent_sync_pattern: PreventSyncPattern::empty(), }); let event_bus = EventBus::default(); let cmds = Arc::new( diff --git a/libparsec/crates/client/tests/unit/workspace/folder_transactions.rs b/libparsec/crates/client/tests/unit/workspace/folder_transactions.rs index 5ee7f8497d1..fe95ae2f118 100644 --- a/libparsec/crates/client/tests/unit/workspace/folder_transactions.rs +++ b/libparsec/crates/client/tests/unit/workspace/folder_transactions.rs @@ -28,7 +28,7 @@ async fn good(#[values(true, false)] root_level: bool, env: &TestbedEnv) { builder.workspace_data_storage_fetch_workspace_vlob( "alice@dev1", wksp1_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); } else { builder @@ -41,7 +41,7 @@ async fn good(#[values(true, false)] root_level: bool, env: &TestbedEnv) { "alice@dev1", wksp1_id, wksp1_foo_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); } }) @@ -466,7 +466,7 @@ async fn bootstrap_env(env: &TestbedEnv, root_level: bool) -> Env { let (base_path, parent_id) = env .customize(|builder| { - let prevent_sync_pattern = Regex::from_regex_str(r"\.tmp$").unwrap(); + let prevent_sync_pattern = PreventSyncPattern::from_regex(r"\.tmp$").unwrap(); let res = if root_level { // Remove all children from the workspace builder @@ -752,7 +752,7 @@ async fn rename_a_entry_to_be_confined( &env.discriminant_dir, &bob, realm_id, - Regex::empty(), // Use empty pattern to prevent workspace to filter out any entry + PreventSyncPattern::empty(), // Use empty pattern to prevent workspace to filter out any entry ) .await; ops_inbound_sync(&bob_ops).await; @@ -890,7 +890,7 @@ async fn rename_a_confined_entry_to_not_be_confined( &env.discriminant_dir, &bob, realm_id, - Regex::empty(), // Use empty pattern to prevent workspace to filter out any entry + PreventSyncPattern::empty(), // Use empty pattern to prevent workspace to filter out any entry ) .await; ops_inbound_sync(&bob_ops).await; @@ -1022,7 +1022,7 @@ async fn rename_a_entry_to_be_confined_with_different_parent( &env.discriminant_dir, &bob, realm_id, - Regex::empty(), // Use empty pattern to prevent workspace to filter out any entry + PreventSyncPattern::empty(), // Use empty pattern to prevent workspace to filter out any entry ) .await; ops_inbound_sync(&bob_ops).await; @@ -1167,7 +1167,7 @@ async fn rename_a_confined_entry_to_not_be_confined_with_different_parent( &env.discriminant_dir, &bob, realm_id, - Regex::empty(), // Use empty pattern to prevent workspace to filter out any entry + PreventSyncPattern::empty(), // Use empty pattern to prevent workspace to filter out any entry ) .await; ops_inbound_sync(&bob_ops).await; diff --git a/libparsec/crates/client/tests/unit/workspace/merge_folder.rs b/libparsec/crates/client/tests/unit/workspace/merge_folder.rs index 389f6e85c96..a9f15736dab 100644 --- a/libparsec/crates/client/tests/unit/workspace/merge_folder.rs +++ b/libparsec/crates/client/tests/unit/workspace/merge_folder.rs @@ -20,7 +20,7 @@ async fn no_remote_change( kind: &str, env: &TestbedEnv, ) { - let prevent_sync_pattern = Regex::from_glob_pattern("*.tmp").unwrap(); + let prevent_sync_pattern = PreventSyncPattern::from_glob("*.tmp").unwrap(); let local_author = "alice@dev1".parse().unwrap(); let timestamp = "2021-01-10T00:00:00Z".parse().unwrap(); let vlob_id = VlobID::from_hex("87c6b5fd3b454c94bab51d6af1c6930b").unwrap(); @@ -175,7 +175,7 @@ async fn no_remote_change_but_local_uses_outdated_prevent_sync_pattern( unknown => panic!("Unknown kind: {}", unknown), } - let new_prevent_sync_pattern = Regex::from_glob_pattern("*.tmp").unwrap(); + let new_prevent_sync_pattern = PreventSyncPattern::from_glob("*.tmp").unwrap(); let outcome = merge_local_folder_manifest( local_author, timestamp, @@ -263,7 +263,7 @@ async fn remote_only_change( speculative: false, }; - let prevent_sync_pattern = Regex::from_glob_pattern("*.tmp").unwrap(); + let prevent_sync_pattern = PreventSyncPattern::from_glob("*.tmp").unwrap(); let child_id = VlobID::from_hex("1040c4845fd1451b9c243c93991d9a5e").unwrap(); let confined_id = VlobID::from_hex("9100fa0bfca94e4d96077dd274a243c0").unwrap(); match kind { @@ -658,7 +658,7 @@ async fn local_and_remote_changes( speculative: false, }; - let prevent_sync_pattern = Regex::from_glob_pattern("*.tmp").unwrap(); + let prevent_sync_pattern = PreventSyncPattern::from_glob("*.tmp").unwrap(); match kind { "only_updated_field_modified" => { // Since only `updated` has been modified on local, then diff --git a/libparsec/crates/client/tests/unit/workspace/move_entry.rs b/libparsec/crates/client/tests/unit/workspace/move_entry.rs index 63939a2f442..f0571962569 100644 --- a/libparsec/crates/client/tests/unit/workspace/move_entry.rs +++ b/libparsec/crates/client/tests/unit/workspace/move_entry.rs @@ -183,7 +183,7 @@ async fn src_not_found( "alice@dev1", wksp1_id, wksp1_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); } }) @@ -253,7 +253,7 @@ async fn exchange_but_dst_not_found( "alice@dev1", wksp1_id, wksp1_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); } }) diff --git a/libparsec/crates/client/tests/unit/workspace/utils.rs b/libparsec/crates/client/tests/unit/workspace/utils.rs index feafcdea4bd..16fb722fbc1 100644 --- a/libparsec/crates/client/tests/unit/workspace/utils.rs +++ b/libparsec/crates/client/tests/unit/workspace/utils.rs @@ -20,7 +20,7 @@ pub(crate) async fn workspace_ops_factory( discriminant_dir, device, realm_id, - Regex::from_regex_str(r"\.tmp$").unwrap(), + PreventSyncPattern::from_regex(r"\.tmp$").unwrap(), ) .await } @@ -29,7 +29,7 @@ pub(crate) async fn workspace_ops_with_prevent_sync_pattern_factory( discriminant_dir: &Path, device: &Arc, realm_id: VlobID, - prevent_sync_pattern: Regex, + prevent_sync_pattern: PreventSyncPattern, ) -> WorkspaceOps { let config = Arc::new(ClientConfig { config_dir: discriminant_dir.to_owned(), diff --git a/libparsec/crates/client/tests/unit/workspace_inbound_sync_monitor.rs b/libparsec/crates/client/tests/unit/workspace_inbound_sync_monitor.rs index f1d6280eca2..3de6eaa0617 100644 --- a/libparsec/crates/client/tests/unit/workspace_inbound_sync_monitor.rs +++ b/libparsec/crates/client/tests/unit/workspace_inbound_sync_monitor.rs @@ -188,7 +188,7 @@ async fn real_io_provides_a_starting_event(env: &TestbedEnv) { workspace_storage_cache_size: WorkspaceStorageCacheSize::Default, proxy: ProxyConfig::default(), with_monitors: false, - prevent_sync_pattern: Regex::from_regex_str(r"\.tmp$").unwrap(), + prevent_sync_pattern: PreventSyncPattern::from_regex(r"\.tmp$").unwrap(), }); let event_bus = EventBus::default(); let cmds = Arc::new( diff --git a/libparsec/crates/platform_mountpoint/examples/minimal.rs b/libparsec/crates/platform_mountpoint/examples/minimal.rs index 0cfddd5de97..db68af652ad 100644 --- a/libparsec/crates/platform_mountpoint/examples/minimal.rs +++ b/libparsec/crates/platform_mountpoint/examples/minimal.rs @@ -56,7 +56,7 @@ async fn main() -> ExitCode { workspace_storage_cache_size: WorkspaceStorageCacheSize::Default, proxy: ProxyConfig::default(), with_monitors: false, - prevent_sync_pattern: Regex::empty(), + prevent_sync_pattern: PreventSyncPattern::empty(), }); let client = Client::start(config, event_bus, alice).await.unwrap(); let wksp1_ops = client.start_workspace(wksp1_id).await.unwrap(); diff --git a/libparsec/crates/platform_mountpoint/tests/unit/operations/open_file.rs b/libparsec/crates/platform_mountpoint/tests/unit/operations/open_file.rs index ed8424af83e..f5997568f39 100644 --- a/libparsec/crates/platform_mountpoint/tests/unit/operations/open_file.rs +++ b/libparsec/crates/platform_mountpoint/tests/unit/operations/open_file.rs @@ -333,7 +333,7 @@ async fn read_only_realm( builder.workspace_data_storage_fetch_workspace_vlob( "bob@dev1", wksp1_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); builder.workspace_data_storage_fetch_file_vlob("bob@dev1", wksp1_id, bar_txt_id); }) diff --git a/libparsec/crates/platform_mountpoint/tests/unit/operations/utils.rs b/libparsec/crates/platform_mountpoint/tests/unit/operations/utils.rs index f7003a3bdcb..8efab0680f2 100644 --- a/libparsec/crates/platform_mountpoint/tests/unit/operations/utils.rs +++ b/libparsec/crates/platform_mountpoint/tests/unit/operations/utils.rs @@ -6,7 +6,7 @@ use libparsec_client::{ Client, ClientConfig, EventBus, MountpointMountStrategy, ProxyConfig, WorkspaceStorageCacheSize, }; use libparsec_tests_fixtures::prelude::*; -use libparsec_types::Regex; +use libparsec_types::PreventSyncPattern; #[cfg_attr(target_os = "windows", allow(dead_code))] pub async fn start_client(env: &TestbedEnv, start_as: &'static str) -> Arc { @@ -28,7 +28,7 @@ pub async fn start_client_with_mountpoint_base_dir( workspace_storage_cache_size: WorkspaceStorageCacheSize::Default, proxy: ProxyConfig::default(), with_monitors: false, - prevent_sync_pattern: Regex::empty(), + prevent_sync_pattern: PreventSyncPattern::empty(), }); let device = env.local_device(start_as); Client::start(config, event_bus, device).await.unwrap() diff --git a/libparsec/crates/platform_mountpoint/tests/unit/operations/windows_drive_mount.rs b/libparsec/crates/platform_mountpoint/tests/unit/operations/windows_drive_mount.rs index 057ff4e7ab5..474076a32c5 100644 --- a/libparsec/crates/platform_mountpoint/tests/unit/operations/windows_drive_mount.rs +++ b/libparsec/crates/platform_mountpoint/tests/unit/operations/windows_drive_mount.rs @@ -23,7 +23,7 @@ async fn mount_on_drive(env: &TestbedEnv) { workspace_storage_cache_size: WorkspaceStorageCacheSize::Default, proxy: ProxyConfig::default(), with_monitors: false, - prevent_sync_pattern: Regex::empty(), + prevent_sync_pattern: PreventSyncPattern::empty(), }); let client = Client::start(config, event_bus, alice).await.unwrap(); diff --git a/libparsec/crates/platform_storage/src/native/workspace.rs b/libparsec/crates/platform_storage/src/native/workspace.rs index 1f91748c4f1..934812e72fe 100644 --- a/libparsec/crates/platform_storage/src/native/workspace.rs +++ b/libparsec/crates/platform_storage/src/native/workspace.rs @@ -424,7 +424,10 @@ impl PlatformWorkspaceStorage { }) } - pub async fn set_prevent_sync_pattern(&mut self, pattern: &Regex) -> anyhow::Result { + pub async fn set_prevent_sync_pattern( + &mut self, + pattern: &PreventSyncPattern, + ) -> anyhow::Result { let pattern = pattern.to_string(); let mut transaction = self.conn.begin().await?; @@ -436,11 +439,11 @@ impl PlatformWorkspaceStorage { Ok(fully_applied) } - pub async fn get_prevent_sync_pattern(&mut self) -> anyhow::Result<(Regex, bool)> { + pub async fn get_prevent_sync_pattern(&mut self) -> anyhow::Result<(PreventSyncPattern, bool)> { db_get_prevent_sync_pattern(&mut self.conn) .await .and_then(|(pattern, fully_applied)| { - Regex::from_regex_str(&pattern) + PreventSyncPattern::from_regex(&pattern) .map(|re| (re, fully_applied)) .map_err(anyhow::Error::from) }) @@ -448,7 +451,7 @@ impl PlatformWorkspaceStorage { pub async fn mark_prevent_sync_pattern_fully_applied( &mut self, - pattern: &Regex, + pattern: &PreventSyncPattern, ) -> Result<(), MarkPreventSyncPatternFullyAppliedError> { let pattern = pattern.to_string(); diff --git a/libparsec/crates/platform_storage/src/web/workspace.rs b/libparsec/crates/platform_storage/src/web/workspace.rs index 862980ef781..74435c06ca9 100644 --- a/libparsec/crates/platform_storage/src/web/workspace.rs +++ b/libparsec/crates/platform_storage/src/web/workspace.rs @@ -22,8 +22,6 @@ use crate::{ }, }; -use super::{db, model::PreventSyncPattern}; - #[derive(Debug)] pub(crate) struct PlatformWorkspaceStorage { conn: Arc, @@ -411,10 +409,13 @@ impl PlatformWorkspaceStorage { }) } - pub async fn set_prevent_sync_pattern(&mut self, pattern: &Regex) -> anyhow::Result { - let tx = PreventSyncPattern::write(&self.conn)?; + pub async fn set_prevent_sync_pattern( + &mut self, + pattern: &PreventSyncPattern, + ) -> anyhow::Result { + let tx = super::model::PreventSyncPattern::write(&self.conn)?; - let current = PreventSyncPattern::get(&tx).await?; + let current = super::model::PreventSyncPattern::get(&tx).await?; let regex = pattern.to_string(); match current { @@ -423,34 +424,34 @@ impl PlatformWorkspaceStorage { // Either we don't have a prevent sync pattern in the database // or the pattern is different from the one provided. None | Some(_) => { - let value = PreventSyncPattern { + let value = super::model::PreventSyncPattern { pattern: regex, fully_applied: false, }; value.insert(&tx).await?; - db::commit(tx).await.and(Ok(false)) + super::db::commit(tx).await.and(Ok(false)) } } } - pub async fn get_prevent_sync_pattern(&mut self) -> anyhow::Result<(Regex, bool)> { - let tx = PreventSyncPattern::read(&self.conn)?; - let res = PreventSyncPattern::get(&tx) + pub async fn get_prevent_sync_pattern(&mut self) -> anyhow::Result<(PreventSyncPattern, bool)> { + let tx = super::model::PreventSyncPattern::read(&self.conn)?; + let res = super::model::PreventSyncPattern::get(&tx) .await? .expect("The database should be initialized with a default value"); - let regex = Regex::from_regex_str(&res.pattern).map_err(anyhow::Error::from)?; + let pattern = PreventSyncPattern::from_regex(&res.pattern).map_err(anyhow::Error::from)?; - Ok((regex, res.fully_applied)) + Ok((pattern, res.fully_applied)) } pub async fn mark_prevent_sync_pattern_fully_applied( &mut self, - pattern: &Regex, + pattern: &PreventSyncPattern, ) -> Result<(), MarkPreventSyncPatternFullyAppliedError> { - let tx = PreventSyncPattern::write(&self.conn)?; + let tx = super::model::PreventSyncPattern::write(&self.conn)?; - let current = PreventSyncPattern::get(&tx).await?; + let current = super::model::PreventSyncPattern::get(&tx).await?; let regex = pattern.to_string(); match current { @@ -461,14 +462,14 @@ impl PlatformWorkspaceStorage { return Ok(()); } - let value = PreventSyncPattern { + let value = super::model::PreventSyncPattern { pattern: regex, fully_applied: true, }; value.insert(&tx).await?; - db::commit(tx) + super::db::commit(tx) .await .map_err(MarkPreventSyncPatternFullyAppliedError::Internal) } diff --git a/libparsec/crates/platform_storage/src/workspace.rs b/libparsec/crates/platform_storage/src/workspace.rs index 393ba8498d3..bdccec4bfad 100644 --- a/libparsec/crates/platform_storage/src/workspace.rs +++ b/libparsec/crates/platform_storage/src/workspace.rs @@ -244,17 +244,20 @@ impl WorkspaceStorage { self.platform.debug_dump().await } - pub async fn set_prevent_sync_pattern(&mut self, pattern: &Regex) -> anyhow::Result { + pub async fn set_prevent_sync_pattern( + &mut self, + pattern: &PreventSyncPattern, + ) -> anyhow::Result { self.platform.set_prevent_sync_pattern(pattern).await } - pub async fn get_prevent_sync_pattern(&mut self) -> anyhow::Result<(Regex, bool)> { + pub async fn get_prevent_sync_pattern(&mut self) -> anyhow::Result<(PreventSyncPattern, bool)> { self.platform.get_prevent_sync_pattern().await } pub async fn mark_prevent_sync_pattern_fully_applied( &mut self, - pattern: &Regex, + pattern: &PreventSyncPattern, ) -> Result<(), MarkPreventSyncPatternFullyAppliedError> { self.platform .mark_prevent_sync_pattern_fully_applied(pattern) diff --git a/libparsec/crates/platform_storage/tests/unit/workspace.rs b/libparsec/crates/platform_storage/tests/unit/workspace.rs index 67cee1354a7..f8975c29b6c 100644 --- a/libparsec/crates/platform_storage/tests/unit/workspace.rs +++ b/libparsec/crates/platform_storage/tests/unit/workspace.rs @@ -65,14 +65,14 @@ async fn testbed_support(#[case] fetch_strategy: FetchStrategy, env: &TestbedEnv builder.workspace_data_storage_fetch_workspace_vlob( "alice@dev1", realm_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); builder.workspace_data_storage_fetch_file_vlob("alice@dev1", realm_id, file_id); builder.workspace_data_storage_fetch_folder_vlob( "alice@dev1", realm_id, folder_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); builder.workspace_cache_storage_fetch_block("alice@dev1", realm_id, block_id); @@ -96,14 +96,14 @@ async fn testbed_support(#[case] fetch_strategy: FetchStrategy, env: &TestbedEnv builder.workspace_data_storage_fetch_workspace_vlob( "alice@dev1", realm_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); builder.workspace_data_storage_fetch_file_vlob("alice@dev1", realm_id, file_id); builder.workspace_data_storage_fetch_folder_vlob( "alice@dev1", realm_id, folder_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); } @@ -1109,7 +1109,7 @@ async fn check_prevent_sync_pattern_initialized_with_empty_pattern(env: &Testbed p_assert_eq!( regex, - Regex::from_regex_str(PREVENT_SYNC_PATTERN_EMPTY_PATTERN).unwrap() + PreventSyncPattern::from_regex(PREVENT_SYNC_PATTERN_EMPTY_PATTERN).unwrap() ); assert!(!bool); } @@ -1118,7 +1118,7 @@ async fn check_prevent_sync_pattern_initialized_with_empty_pattern(env: &Testbed async fn mark_empty_pattern_as_fully_applied(env: &TestbedEnv) { let mut workspace = start_workspace(env).await; - let empty_pattern = Regex::from_regex_str(PREVENT_SYNC_PATTERN_EMPTY_PATTERN).unwrap(); + let empty_pattern = PreventSyncPattern::from_regex(PREVENT_SYNC_PATTERN_EMPTY_PATTERN).unwrap(); let res = workspace .mark_prevent_sync_pattern_fully_applied(&empty_pattern) @@ -1133,7 +1133,7 @@ async fn check_set_pattern_is_idempotent(env: &TestbedEnv) { let mut workspace = start_workspace(env).await; // 1st, mark the empty pattern as fully applied. - let empty_pattern = Regex::from_regex_str(PREVENT_SYNC_PATTERN_EMPTY_PATTERN).unwrap(); + let empty_pattern = PreventSyncPattern::from_regex(PREVENT_SYNC_PATTERN_EMPTY_PATTERN).unwrap(); let res = workspace .mark_prevent_sync_pattern_fully_applied(&empty_pattern) @@ -1154,7 +1154,7 @@ async fn check_set_pattern_is_idempotent(env: &TestbedEnv) { async fn set_prevent_sync_pattern(env: &TestbedEnv) { let mut workspace = start_workspace(env).await; - let regex = Regex::from_regex_str(r".*\.tmp$").unwrap(); + let regex = PreventSyncPattern::from_regex(r".*\.tmp$").unwrap(); let res = workspace.set_prevent_sync_pattern(®ex).await.unwrap(); @@ -1169,7 +1169,7 @@ async fn set_prevent_sync_pattern(env: &TestbedEnv) { async fn nop_mark_prevent_sync_pattern_with_different_pat(env: &TestbedEnv) { let mut workspace = start_workspace(env).await; - let regex = Regex::from_regex_str(r".*\.tmp$").unwrap(); + let regex = PreventSyncPattern::from_regex(r".*\.tmp$").unwrap(); let res = workspace .mark_prevent_sync_pattern_fully_applied(®ex) diff --git a/libparsec/crates/testbed/src/template/build.rs b/libparsec/crates/testbed/src/template/build.rs index 06b52213024..156e55d7d11 100644 --- a/libparsec/crates/testbed/src/template/build.rs +++ b/libparsec/crates/testbed/src/template/build.rs @@ -398,7 +398,7 @@ impl TestbedTemplateBuilder { &mut self, device: impl TryInto, realm: impl TryInto, - prevent_sync_pattern: impl TryInto, + prevent_sync_pattern: impl TryInto, ) -> TestbedEventWorkspaceDataStorageFetchFolderVlobBuilder { let realm: VlobID = realm .try_into() @@ -1171,7 +1171,7 @@ impl_event_builder!( device: DeviceID, realm: VlobID, vlob: VlobID, - prevent_sync_pattern: Regex + prevent_sync_pattern: PreventSyncPattern ] ); diff --git a/libparsec/crates/testbed/src/template/events.rs b/libparsec/crates/testbed/src/template/events.rs index 7aca0251cfc..64b82ef51ec 100644 --- a/libparsec/crates/testbed/src/template/events.rs +++ b/libparsec/crates/testbed/src/template/events.rs @@ -3424,7 +3424,7 @@ impl TestbedEventWorkspaceDataStorageFetchFolderVlob { device: DeviceID, realm: VlobID, vlob: VlobID, - prevent_sync_pattern: Regex, + prevent_sync_pattern: PreventSyncPattern, ) -> Self { // 1) Consistency checks diff --git a/libparsec/crates/testbed/src/templates/coolorg.rs b/libparsec/crates/testbed/src/templates/coolorg.rs index 4fc947fdf28..d0b3b3bfb09 100644 --- a/libparsec/crates/testbed/src/templates/coolorg.rs +++ b/libparsec/crates/testbed/src/templates/coolorg.rs @@ -75,7 +75,7 @@ pub(crate) fn generate() -> Arc { builder.workspace_data_storage_fetch_workspace_vlob( "alice@dev1", wksp1_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); builder.workspace_data_storage_fetch_realm_checkpoint("alice@dev1", wksp1_id); @@ -87,7 +87,7 @@ pub(crate) fn generate() -> Arc { builder.workspace_data_storage_fetch_workspace_vlob( "bob@dev1", wksp1_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); builder.workspace_data_storage_fetch_realm_checkpoint("bob@dev1", wksp1_id); diff --git a/libparsec/crates/testbed/src/templates/minimal_client_ready.rs b/libparsec/crates/testbed/src/templates/minimal_client_ready.rs index ea2034e4fd2..f7b10be211e 100644 --- a/libparsec/crates/testbed/src/templates/minimal_client_ready.rs +++ b/libparsec/crates/testbed/src/templates/minimal_client_ready.rs @@ -114,7 +114,7 @@ pub(crate) fn generate() -> Arc { builder.workspace_data_storage_fetch_workspace_vlob( "alice@dev1", wksp1_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); builder.workspace_data_storage_fetch_file_vlob("alice@dev1", wksp1_id, bar_txt_id); builder.workspace_cache_storage_fetch_block("alice@dev1", wksp1_id, bar_txt_block_id); @@ -122,13 +122,13 @@ pub(crate) fn generate() -> Arc { "alice@dev1", wksp1_id, foo_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); builder.workspace_data_storage_fetch_folder_vlob( "alice@dev1", wksp1_id, foo_spam_id, - libparsec_types::Regex::empty(), + libparsec_types::PreventSyncPattern::empty(), ); builder.workspace_data_storage_fetch_file_vlob("alice@dev1", wksp1_id, foo_egg_txt_id); builder.workspace_data_storage_fetch_realm_checkpoint("alice@dev1", wksp1_id); diff --git a/libparsec/crates/types/src/error.rs b/libparsec/crates/types/src/error.rs index 5cc42f07082..08cd7e3f22a 100644 --- a/libparsec/crates/types/src/error.rs +++ b/libparsec/crates/types/src/error.rs @@ -8,21 +8,6 @@ use libparsec_crypto::CryptoError; use crate::{DateTime, DeviceFileType, DeviceID, HumanHandle, UserID, VlobID}; -#[derive(Error, Debug)] -pub enum RegexError { - #[error("Regex parsing err: {err}")] - ParseError { err: regex::Error }, - #[error("Failed to convert glob pattern into regex: {err}")] - GlobPatternError { err: fnmatch_regex::error::Error }, - #[error("IO error on pattern file `{file_path}`: {err}")] - PatternFileIOError { - file_path: std::path::PathBuf, - err: std::io::Error, - }, -} - -pub type RegexResult = Result; - #[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum DataError { #[error("Invalid encryption")] diff --git a/libparsec/crates/types/src/lib.rs b/libparsec/crates/types/src/lib.rs index 079066f2d60..25b940fc70c 100644 --- a/libparsec/crates/types/src/lib.rs +++ b/libparsec/crates/types/src/lib.rs @@ -69,16 +69,15 @@ mod local_manifest; mod manifest; mod organization; mod pki; +mod prevent_sync_pattern; mod protocol; mod realm; -mod regex; mod sas_code; mod serialization; mod shamir; mod time; mod token; -pub use crate::regex::*; pub use addr::*; pub use certif::*; pub use error::*; @@ -92,6 +91,7 @@ pub use local_manifest::*; pub use manifest::*; pub use organization::*; pub use pki::*; +pub use prevent_sync_pattern::*; pub use protocol::*; pub use realm::*; pub use sas_code::*; diff --git a/libparsec/crates/types/src/local_manifest/folder.rs b/libparsec/crates/types/src/local_manifest/folder.rs index 8454db8db06..39612e5f470 100644 --- a/libparsec/crates/types/src/local_manifest/folder.rs +++ b/libparsec/crates/types/src/local_manifest/folder.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::{ self as libparsec_types, impl_transparent_data_format_conversion, DataError, DataResult, - DateTime, DeviceID, EntryName, FolderManifest, Regex, VlobID, + DateTime, DeviceID, EntryName, FolderManifest, PreventSyncPattern, VlobID, }; use super::{impl_local_manifest_dump, impl_local_manifest_load}; @@ -165,7 +165,7 @@ impl LocalFolderManifest { pub fn evolve_children_and_mark_updated( &mut self, data: HashMap>, - prevent_sync_pattern: &Regex, + prevent_sync_pattern: &PreventSyncPattern, timestamp: DateTime, ) { let mut actually_updated = false; @@ -217,7 +217,7 @@ impl LocalFolderManifest { pub fn apply_prevent_sync_pattern( &self, - prevent_sync_pattern: &Regex, + prevent_sync_pattern: &PreventSyncPattern, timestamp: DateTime, ) -> Self { UnconfinedLocalFolderManifest::remove_confinement(self).apply_confinement( @@ -227,7 +227,7 @@ impl LocalFolderManifest { ) } - pub fn from_remote(remote: FolderManifest, prevent_sync_pattern: &Regex) -> Self { + pub fn from_remote(remote: FolderManifest, prevent_sync_pattern: &PreventSyncPattern) -> Self { UnconfinedLocalFolderManifest::apply_confinement_from_remote(remote, prevent_sync_pattern) } @@ -246,7 +246,7 @@ impl LocalFolderManifest { /// set to the provided `timestamp`. pub fn from_remote_with_restored_local_confinement_points( remote: FolderManifest, - prevent_sync_pattern: &Regex, + prevent_sync_pattern: &PreventSyncPattern, local_manifest: &Self, timestamp: DateTime, ) -> Self { @@ -302,7 +302,7 @@ impl UnconfinedLocalFolderManifest { /// local manifest to merge with. Otherwise, use `apply_confinement`. pub fn apply_confinement_from_remote( remote: FolderManifest, - prevent_sync_pattern: &Regex, + prevent_sync_pattern: &PreventSyncPattern, ) -> LocalFolderManifest { // Filter out the base entries that matches the prevent sync pattern let mut new_children = remote.children.clone(); @@ -347,7 +347,7 @@ impl UnconfinedLocalFolderManifest { pub fn apply_confinement( self, existing_local_manifest: &LocalFolderManifest, - prevent_sync_pattern: &Regex, + prevent_sync_pattern: &PreventSyncPattern, timestamp: DateTime, ) -> LocalFolderManifest { // Filter out the base entries that matches the prevent sync pattern diff --git a/libparsec/crates/types/src/prevent_sync_pattern.rs b/libparsec/crates/types/src/prevent_sync_pattern.rs new file mode 100644 index 00000000000..4396c6aa417 --- /dev/null +++ b/libparsec/crates/types/src/prevent_sync_pattern.rs @@ -0,0 +1,124 @@ +// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS + +use std::{collections::HashSet, fmt::Display}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PreventSyncPatternError { + #[error("Regex parsing error: {0}")] + BadRegex(regex::Error), + #[error("Glob parsing error: {0}")] + BadGlob(fnmatch_regex::error::Error), +} + +pub type PreventSyncPatternResult = Result; + +#[derive(Clone, Debug)] +pub struct PreventSyncPattern(pub Vec); + +const DEFAULT_PREVENT_SYNC_PATTERN: &str = std::include_str!("default_pattern.ignore"); + +impl Default for PreventSyncPattern { + fn default() -> Self { + Self::from_glob_ignore_file(DEFAULT_PREVENT_SYNC_PATTERN) + .expect("Cannot parse default prevent sync pattern") + } +} + +impl PreventSyncPattern { + /// Glob ignore file format is simply: + /// - Each line contains either a glob pattern of a comment + /// - Each line is first trimmed of leading/trailing whitespaces + /// - Comment lines start with a `#` character + /// + /// Example: + /// ```raw + /// # Ignore C stuff + /// *.{so,o} + /// + /// # Ignore Python stuff + /// *.pyc + /// ``` + /// + /// (see `default_pattern.ignore` for a more complex example) + pub fn from_glob_ignore_file(file_content: &str) -> PreventSyncPatternResult { + let regexes = file_content + .lines() + .filter_map(|line| { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + None + } else { + Some(regex_from_glob_pattern(line)) + } + }) + .collect::>()?; + Ok(Self(regexes)) + } + + /// Create a prevent sync pattern from a regex (regular expression, e.g. `^.*\.(txt|md)$`) + pub fn from_regex(regex: &str) -> PreventSyncPatternResult { + let regex = regex::Regex::new(regex).map_err(PreventSyncPatternError::BadRegex)?; + Ok(Self(vec![regex])) + } + + /// Create a prevent sync pattern from a glob pattern (e.g. `*.{txt,md}`) + pub fn from_glob(pattern: &str) -> PreventSyncPatternResult { + regex_from_glob_pattern(pattern).map(|re| Self(vec![re])) + } + + pub fn from_multiple_globs<'a>( + patterns: impl Iterator, + ) -> PreventSyncPatternResult { + let regexes = patterns + .map(regex_from_glob_pattern) + .collect::>()?; + Ok(Self(regexes)) + } + + pub fn is_match(&self, string: &str) -> bool { + self.0.iter().any(|r| r.is_match(string)) + } + + /// Create an empty prevent sync pattern that will never match anything + pub const fn empty() -> Self { + Self(Vec::new()) + } +} + +/// Parse a glob pattern like `*.rs` and convert it to an regex. +fn regex_from_glob_pattern(pattern: &str) -> PreventSyncPatternResult { + fnmatch_regex::glob_to_regex(pattern).map_err(PreventSyncPatternError::BadGlob) +} + +impl PartialEq for PreventSyncPattern { + // This overload is only used in python tests. We are not using HashSet directly + // in the `Regex` type because `regex::Regex` has no implementation of `Hash`. + fn eq(&self, other: &Self) -> bool { + if self.0.len() == other.0.len() { + let set: HashSet<&str> = HashSet::from_iter(self.0.iter().map(|r| r.as_str())); + + other.0.iter().all(|r| set.contains(r.as_str())) + } else { + false + } + } +} + +impl Display for PreventSyncPattern { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + self.0 + .iter() + .map(regex::Regex::as_str) + .collect::>() + .join("|") + ) + } +} + +#[cfg(test)] +#[path = "../tests/unit/prevent_sync_pattern.rs"] +mod tests; diff --git a/libparsec/crates/types/src/regex.rs b/libparsec/crates/types/src/regex.rs deleted file mode 100644 index b5762276bb7..00000000000 --- a/libparsec/crates/types/src/regex.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS - -use std::{ - collections::HashSet, - fmt::Display, - fs, - io::{BufRead, BufReader}, - path::Path, -}; - -use crate::{RegexError, RegexResult}; - -#[derive(Clone, Debug)] -pub struct Regex(pub Vec); - -const DEFAULT_PREVENT_SYNC_PATTERN: &str = std::include_str!("default_pattern.ignore"); - -impl Default for Regex { - fn default() -> Self { - Self::from_glob_reader( - stringify!(DEFAULT_PREVENT_SYNC_PATTERN), - std::io::Cursor::new(DEFAULT_PREVENT_SYNC_PATTERN), - ) - .expect("Cannot parse default prevent sync pattern") - } -} - -/// The fnmatch_regex crate does not escape the character '$'. It is a problem -/// for us. Since we need to match strings like `$RECYCLE.BIN` we need to escape -/// it anyway. -fn escape_globing_pattern(string: &str) -> String { - str::replace(string, "$", "\\$") -} - -impl Regex { - /// Returns a regex which is built from a file that contains shell like patterns - pub fn from_file(file_path: &Path) -> RegexResult { - let reader = fs::File::open(file_path).map(BufReader::new).map_err(|e| { - RegexError::PatternFileIOError { - file_path: file_path.to_path_buf(), - err: e, - } - })?; - - Self::from_glob_reader(file_path, reader) - } - - pub fn from_glob_reader, R: BufRead>(path: P, reader: R) -> RegexResult { - reader - .lines() - .filter_map(|line| match line { - Ok(line) => { - let l = line.trim(); - - if l != "\n" && !l.starts_with('#') { - Some(from_glob_pattern(l)) - } else { - None - } - } - Err(e) => Some(Err(RegexError::PatternFileIOError { - file_path: path.as_ref().to_path_buf(), - err: e, - })), - }) - .collect::>>() - .map(Self) - } - - /// Returns a regex which is an union of all regexes from `raw_regexes` slice parameter - pub fn from_raw_regexes(raw_regexes: &[&str]) -> RegexResult { - Ok(Self( - raw_regexes - .iter() - .map(|l| regex::Regex::new(l).map_err(|err| RegexError::ParseError { err })) - .collect::, RegexError>>()?, - )) - } - - /// Returns a regex from a glob pattern - pub fn from_glob_pattern(pattern: &str) -> RegexResult { - from_glob_pattern(pattern).map(|re| Self(vec![re])) - } - - pub fn from_regex_str(regex_str: &str) -> RegexResult { - Self::from_raw_regexes(&[regex_str]) - } - - pub fn is_match(&self, string: &str) -> bool { - self.0.iter().any(|r| r.is_match(string)) - } - - /// Create an empty regex, that regex will never match anything - pub const fn empty() -> Self { - Self(Vec::new()) - } -} - -/// Parse a glob pattern like `*.rs` and convert it to an regex. -fn from_glob_pattern(pattern: &str) -> RegexResult { - let escaped_str = escape_globing_pattern(pattern); - fnmatch_regex::glob_to_regex(&escaped_str).map_err(|err| RegexError::GlobPatternError { err }) -} - -impl PartialEq for Regex { - // This overload is only used in python tests. We are not using HashSet directly - // in the `Regex` type because `regex::Regex` has no implementation of `Hash`. - fn eq(&self, other: &Self) -> bool { - if self.0.len() == other.0.len() { - let set: HashSet<&str> = HashSet::from_iter(self.0.iter().map(|r| r.as_str())); - - other.0.iter().all(|r| set.contains(r.as_str())) - } else { - false - } - } -} - -impl Display for Regex { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - self.0 - .iter() - .map(regex::Regex::as_str) - .collect::>() - .join("|") - ) - } -} - -#[cfg(test)] -#[path = "../tests/unit/regex.rs"] -mod tests; diff --git a/libparsec/crates/types/tests/unit/local_folder_manifest.rs b/libparsec/crates/types/tests/unit/local_folder_manifest.rs index d72709150c3..3b8ca1ad932 100644 --- a/libparsec/crates/types/tests/unit/local_folder_manifest.rs +++ b/libparsec/crates/types/tests/unit/local_folder_manifest.rs @@ -276,7 +276,7 @@ fn new_root(#[values(true, false)] speculative: bool, timestamp: DateTime) { ("file1.png".parse().unwrap(), VlobID::from_hex("936DA01F9ABD4d9d80C702AF85C822A8").unwrap()) ]), 0, - ".mp4", + r"\.mp4$", ))] fn from_remote( timestamp: DateTime, @@ -299,7 +299,10 @@ fn from_remote( children, }; - let lfm = LocalFolderManifest::from_remote(fm.clone(), &Regex::from_regex_str(regex).unwrap()); + let lfm = LocalFolderManifest::from_remote( + fm.clone(), + &PreventSyncPattern::from_regex(regex).unwrap(), + ); p_assert_eq!(lfm.base, fm); assert!(!lfm.need_sync); @@ -376,7 +379,7 @@ fn from_remote( // The pattern doesn't match the one used in the local manifest, hence the need_sync // is set to true (the previously confined data must now be synchronized !) and there // is no local confinement points anymore - ".tmp~", + r"\.tmp~$", )] // Note there is no `remote_children_confined_with_outdated_pattern` test given the // remote confinement points are just ignored by `LocalFolderManifest::from_remote_with_restored_local_confinement_points` @@ -474,7 +477,7 @@ fn from_remote_with_restored_local_confinement_points( let lfm = LocalFolderManifest::from_remote_with_restored_local_confinement_points( fm.clone(), - &Regex::from_regex_str(regex).unwrap(), + &PreventSyncPattern::from_regex(regex).unwrap(), &lfm, timestamp, ); @@ -692,7 +695,7 @@ fn evolve_children_and_mark_updated( #[case] expected_local_confinement_points: HashSet, #[case] expected_need_sync: bool, ) { - let prevent_sync_pattern = Regex::from_regex_str(PREVENT_SYNC_PATTERN_TMP).unwrap(); + let prevent_sync_pattern = PreventSyncPattern::from_regex(PREVENT_SYNC_PATTERN_TMP).unwrap(); let t1 = "2000-01-01T00:00:00Z".parse().unwrap(); let t2 = "2000-01-02T00:00:00Z".parse().unwrap(); @@ -795,7 +798,8 @@ fn apply_prevent_sync_pattern_nothing_confined() { }; // New prevent sync pattern doesn't match any entry, so nothing should change - let new_prevent_sync_pattern = Regex::from_regex_str(PREVENT_SYNC_PATTERN_TMP).unwrap(); + let new_prevent_sync_pattern = + PreventSyncPattern::from_regex(PREVENT_SYNC_PATTERN_TMP).unwrap(); let lfm2 = lfm.apply_prevent_sync_pattern(&new_prevent_sync_pattern, t3); p_assert_eq!(lfm2, lfm); @@ -862,7 +866,7 @@ fn apply_prevent_sync_pattern_stability_with_confined() { VlobID::from_hex("B0C37F14927244FA8550EDAECEA09E96").unwrap(), ), ]), - // Current prevent sync pattern is `.tmp` + // Current prevent sync pattern is `\.tmp$` local_confinement_points: HashSet::from_iter([VlobID::from_hex( "B0C37F14927244FA8550EDAECEA09E96", ) @@ -874,8 +878,9 @@ fn apply_prevent_sync_pattern_stability_with_confined() { speculative: false, }; - // We re-apply the same `.tmp` prevent sync pattern, so nothing should change - let current_prevent_sync_pattern = Regex::from_regex_str(PREVENT_SYNC_PATTERN_TMP).unwrap(); + // We re-apply the same `\.tmp$` prevent sync pattern, so nothing should change + let current_prevent_sync_pattern = + PreventSyncPattern::from_regex(PREVENT_SYNC_PATTERN_TMP).unwrap(); let lfm2 = lfm.apply_prevent_sync_pattern(¤t_prevent_sync_pattern, t3); p_assert_eq!(lfm2, lfm); @@ -899,8 +904,8 @@ fn apply_prevent_sync_pattern_with_non_confined_local_children_matching_future_p }; // Create a local folder manifest without any confinement points (the local children - // have names ending `.tmp`, but we can consider the current prevent sync pattern is - // something else for now). + // have names ending in `.tmp`, but we can consider the current prevent sync pattern + // is something else for now). let lfm = LocalFolderManifest { base: fm.clone(), parent: fm.parent, @@ -923,7 +928,8 @@ fn apply_prevent_sync_pattern_with_non_confined_local_children_matching_future_p }; // Now we change the prevent sync pattern to something that matches some of the local children - let new_prevent_sync_pattern = Regex::from_regex_str(PREVENT_SYNC_PATTERN_TMP).unwrap(); + let new_prevent_sync_pattern = + PreventSyncPattern::from_regex(PREVENT_SYNC_PATTERN_TMP).unwrap(); let lfm = lfm.apply_prevent_sync_pattern(&new_prevent_sync_pattern, t3); p_assert_eq!(lfm.remote_confinement_points, HashSet::new()); @@ -957,8 +963,8 @@ fn apply_prevent_sync_pattern_with_non_confined_remote_children_matching_future_ let t3 = "2000-01-03T00:00:00Z".parse().unwrap(); // Create a local folder manifest without any confinement points (the local children - // have names ending `.tmp`, but we can consider the current prevent sync pattern is - // something else for now). + // have names ending in `.tmp`, but we can consider the current prevent sync pattern + // is something else for now). let fm = FolderManifest { author: DeviceID::default(), @@ -1007,7 +1013,8 @@ fn apply_prevent_sync_pattern_with_non_confined_remote_children_matching_future_ }; // Now we change the prevent sync pattern to something that matches some of the remote children - let new_prevent_sync_pattern = Regex::from_regex_str(PREVENT_SYNC_PATTERN_TMP).unwrap(); + let new_prevent_sync_pattern = + PreventSyncPattern::from_regex(PREVENT_SYNC_PATTERN_TMP).unwrap(); let lfm = lfm.apply_prevent_sync_pattern(&new_prevent_sync_pattern, t3); p_assert_eq!( @@ -1060,7 +1067,7 @@ fn apply_prevent_sync_pattern_with_confined_local_children_turning_non_confined( VlobID::from_hex("936DA01F9ABD4d9d80C702AF85C822A8").unwrap(), ), ]), - // Current prevent sync pattern is `.tmp` + // Current prevent sync pattern is `\.tmp$` local_confinement_points: HashSet::from_iter([VlobID::from_hex( "3DF3AC53967C43D889860AE2F459F42B", ) @@ -1071,7 +1078,7 @@ fn apply_prevent_sync_pattern_with_confined_local_children_turning_non_confined( // The new prevent sync pattern doesn't match any entry, hence `fileA.tmp` is // no longer confined, hence manifest's `updated` field should also get updated. - let new_prevent_sync_pattern = Regex::from_regex_str(".tmp~").unwrap(); + let new_prevent_sync_pattern = PreventSyncPattern::from_regex(r"\.tmp~$").unwrap(); let lfm = lfm.apply_prevent_sync_pattern(&new_prevent_sync_pattern, t3); p_assert_eq!(lfm.remote_confinement_points, HashSet::new()); @@ -1149,7 +1156,7 @@ fn apply_prevent_sync_pattern_with_local_changes_and_confined_remote_children_tu // Manifest is still need sync due to the removal of `file2.txt`, however the // `updated` shouldn't has changed since the change in confinement is on // an entry that is already in remote manifest ! - let new_prevent_sync_pattern = Regex::from_regex_str(".tmp~").unwrap(); + let new_prevent_sync_pattern = PreventSyncPattern::from_regex(r"\.tmp~$").unwrap(); let lfm = lfm.apply_prevent_sync_pattern(&new_prevent_sync_pattern, t3); p_assert_eq!(lfm.remote_confinement_points, HashSet::new()); @@ -1218,7 +1225,7 @@ fn apply_prevent_sync_pattern_with_only_confined_remote_children_turning_non_con // The new prevent sync pattern doesn't match any entry, hence `file3.tmp` is // no longer confined, but manifest doesn't need to be sync since this // entry is already in remote manifest ! - let new_prevent_sync_pattern = Regex::from_regex_str(".tmp~").unwrap(); + let new_prevent_sync_pattern = PreventSyncPattern::from_regex(r"\.tmp~$").unwrap(); let lfm = lfm.apply_prevent_sync_pattern(&new_prevent_sync_pattern, t2); p_assert_eq!(lfm.remote_confinement_points, HashSet::new()); @@ -1292,7 +1299,7 @@ fn apply_prevent_sync_pattern_with_broader_prevent_sync_pattern() { VlobID::from_hex("B0C37F14927244FA8550EDAECEA09E96").unwrap(), ), ]), - // Current prevent sync pattern is `.tmp` + // Current prevent sync pattern is `\.tmp$` local_confinement_points: HashSet::from_iter([VlobID::from_hex( "B0C37F14927244FA8550EDAECEA09E96", ) @@ -1304,9 +1311,9 @@ fn apply_prevent_sync_pattern_with_broader_prevent_sync_pattern() { speculative: false, }; - // `.+` is a superset of the previous `.tmp` pattern, all entries should + // `.+` is a superset of the previous `\.tmp$` pattern, all entries should // be confined now - let new_prevent_sync_pattern = Regex::from_regex_str(".+").unwrap(); + let new_prevent_sync_pattern = PreventSyncPattern::from_regex(".+").unwrap(); let lfm = lfm.apply_prevent_sync_pattern(&new_prevent_sync_pattern, t3); p_assert_eq!( @@ -1433,7 +1440,7 @@ fn apply_prevent_sync_pattern_on_renamed_entry( }; // Now apply the new prevent sync pattern... - let new_prevent_sync_pattern = Regex::from_regex_str(".tmp~").unwrap(); + let new_prevent_sync_pattern = PreventSyncPattern::from_regex(r"\.tmp~$").unwrap(); let lfm = lfm.apply_prevent_sync_pattern(&new_prevent_sync_pattern, t3); p_assert_eq!( diff --git a/libparsec/crates/types/tests/unit/prevent_sync_pattern.rs b/libparsec/crates/types/tests/unit/prevent_sync_pattern.rs new file mode 100644 index 00000000000..9886429e0af --- /dev/null +++ b/libparsec/crates/types/tests/unit/prevent_sync_pattern.rs @@ -0,0 +1,123 @@ +// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS + +use libparsec_tests_lite::prelude::*; + +use crate::{prevent_sync_pattern::PreventSyncPattern, PreventSyncPatternError}; + +#[rstest] +#[case::base("*.rs\n*.py")] +#[case::trim_whitespace(" *.rs\n *.py ")] +#[case::empty_lines("*.rs\n\n\n*.py")] +#[case::trim_whitespace_and_empty_lines(" *.rs\n\n \n\n*.py ")] +#[case::ignore_comment("# This contains patterns\n## yes\n *.rs\n\n \n\n*.py ")] +fn from_glob_ignore_file(#[case] file_content: &str) { + let pattern = PreventSyncPattern::from_glob_ignore_file(file_content).unwrap(); + + assert!(pattern.is_match("file.py")); + assert!(pattern.is_match("file.rs")); + assert!(!pattern.is_match("stories.txt")); +} + +#[test] +fn load_default_pattern_file() { + let pattern = PreventSyncPattern::default(); + + for candidate in &[ + "file.swp", + "file~", + "~$file.docx", + "$RECYCLE.BIN", + "desktop.ini", + "shortcut.lnk", + ] { + assert!(pattern.is_match(candidate), "{:?} should match", candidate); + } + + for candidate in &["secrets.txt", "a.docx", "picture.png"] { + assert!( + !pattern.is_match(candidate), + "{:?} should not match", + candidate + ); + } +} + +#[rstest] +#[case::match_wildcard("*.rs", "foo.rs", true)] +#[case::no_match_wildcard_differs("*.rs", "foo.py", false)] +#[case::match_wildcard_literal("*.rs", "*.rs", true)] +#[case::no_match_sub_extension("*.rs", "foo.rs.py", false)] +#[case::match_sub_extension("*.tar.gz", "foo.tar.gz", true)] +#[case::no_match_missing_wildcard(".py", "foo.py", false)] +#[case::match_with_wildcard_empty("*.rs", ".rs", true)] +#[case::match_multi_wildcard("*bar*", "foobar.rs", true)] +#[case::match_multi_wildcard_empty("*bar*", "bar", true)] +#[case::match_multi_wildcard_suffix("*bar*", "foo.bar", true)] +#[case::no_match_multi_wildcard("*bar*", "dummy", false)] +#[case::match_question("fo?bar", "foobar", true)] +#[case::match_question_literal("fo?bar", "fo?bar", true)] +#[case::no_match_question_missing("fo?bar", "fobar", false)] // cspell:disable-line +#[case::match_caret("^foo.bar^", "^foo.bar^", true)] +#[case::no_match_caret("^foo", "foo", false)] +#[case::match_dollar("$foo.bar$", "$foo.bar$", true)] +#[case::no_match_dollar("foo$", "foo", false)] +#[case::no_match_empty("", "foo", false)] +#[case::match_empty("", "", true)] +fn glob_pattern(#[case] glob: &str, #[case] candidate: &str, #[case] expected_match: bool) { + let pattern = PreventSyncPattern::from_glob(glob).unwrap(); + p_assert_eq!(pattern.is_match(candidate), expected_match); +} + +#[test] +fn from_multiple_globs() { + let pattern = + PreventSyncPattern::from_multiple_globs(["", "foo", "*.bar"].into_iter()).unwrap(); + + assert!(pattern.is_match("")); + assert!(pattern.is_match("foo")); + assert!(pattern.is_match("foo.bar")); + + assert!(!pattern.is_match(".foo")); +} + +#[rstest] +#[case::match_simple(r"fooo?$", "foo", true)] +#[case::match_complex(r"^spam.*fooo?$", "spambarfoo", true)] // cspell:disable-line +#[case::no_match_simple(r"fooo?$", "fo", false)] +fn regex_pattern(#[case] regex: &str, #[case] candidate: &str, #[case] expected_match: bool) { + let pattern = PreventSyncPattern::from_regex(regex).unwrap(); + p_assert_eq!(pattern.is_match(candidate), expected_match); +} + +#[test] +fn bad_regex() { + p_assert_matches!( + PreventSyncPattern::from_regex(r"fooo][?"), + Err(PreventSyncPatternError::BadRegex { .. }) + ); +} + +#[test] +fn bad_glob() { + p_assert_matches!( + PreventSyncPattern::from_glob(r"fooo][?"), + Err(PreventSyncPatternError::BadGlob { .. }) + ); +} + +#[test] +fn bad_multiple_globs() { + p_assert_matches!( + PreventSyncPattern::from_multiple_globs(["", "fooo][?"].into_iter()), + Err(PreventSyncPatternError::BadGlob { .. }) + ); +} + +#[test] +fn display() { + let pattern = PreventSyncPattern::from_multiple_globs(["foo.*", "bar?"].into_iter()).unwrap(); + p_assert_eq!( + format!("{:?}", pattern), + r#"PreventSyncPattern([Regex("^foo\\..*$"), Regex("^bar[^/]$")])"# + ); +} diff --git a/libparsec/crates/types/tests/unit/regex.rs b/libparsec/crates/types/tests/unit/regex.rs deleted file mode 100644 index ec009b710bf..00000000000 --- a/libparsec/crates/types/tests/unit/regex.rs +++ /dev/null @@ -1,92 +0,0 @@ -// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS - -use std::{ - io::{BufReader, Cursor}, - path::Path, -}; - -use libparsec_tests_lite::prelude::*; - -use crate::regex::Regex; - -#[rstest] -#[case::base("*.rs\n*.py", "base.tmp")] -#[case::trim_whitespace(" *.rs\n *.py ", "trim_whitespace.tmp")] -#[case::empty_lines("*.rs\n\n\n*.py", "empty_lines.tmp")] -#[case::trim_whitespace_and_empty_lines( - " *.rs\n\n \n\n*.py ", - "trim_whitespace_and_empty_lines.tmp" -)] -#[case::ignore_comment( - "# This contains patterns\n## yes\n *.rs\n\n \n\n*.py ", - "ignore_comment.tmp" -)] -fn from_pattern_file_content(#[case] file_content: &str, #[case] filename: &str) { - let reader = Cursor::new(file_content.to_string()); - let regex = - Regex::from_glob_reader(filename, BufReader::new(reader)).expect("Regex should be valid"); - - assert!(regex.is_match("file.py")); - assert!(regex.is_match("file.rs")); - assert!(!regex.is_match("stories.txt")); -} - -#[test] -fn load_default_pattern_file() { - let regex = Regex::from_file(Path::new("src/default_pattern.ignore")) - .expect("Load default pattern file failed"); - - for pattern in &[ - "file.swp", - "file~", - "$RECYCLE.BIN", - "desktop.ini", - "shortcut.lnk", - ] { - assert!(regex.is_match(pattern), "Pattern {} should match", pattern); - } - - for pattern in &["secrets.txt", "a.docx", "picture.png"] { - assert!( - !regex.is_match(pattern), - "Pattern {} should not match", - pattern - ); - } -} - -#[rstest] -#[case("*.rs", "foo.rs", "bar.py", "bar.rs")] -#[case("*bar*", "foobar.rs", "dummy", "foobarrrrr.py")] -#[case("$myf$le.*", "$myf$le.txt", "bar", "$myf$le.rs")] -fn wildcard_pattern( - #[case] pattern_str: &str, - #[case] valid_case: &str, - #[case] bad_base: &str, - #[case] other_case: &str, -) { - let regex = Regex::from_glob_pattern(pattern_str).expect("Should be valid"); - assert!(regex.is_match(valid_case)); - assert!(!regex.is_match(bad_base)); - assert!(regex.is_match(other_case)); -} - -#[rstest] -#[case(r"fooo?$", "foo", "fo", "fooo")] -fn regex_pattern( - #[case] regex_str: &str, - #[case] valid_case: &str, - #[case] bad_base: &str, - #[case] other_case: &str, -) { - let regex = Regex::from_regex_str(regex_str).expect("Should be valid"); - assert!(regex.is_match(valid_case)); - assert!(!regex.is_match(bad_base)); - assert!(regex.is_match(other_case)); -} - -#[test] -fn bad_regex_str_creation() { - let r = crate::regex::Regex::from_regex_str(r"fooo][?"); - assert!(r.is_err()); -} diff --git a/libparsec/src/config.rs b/libparsec/src/config.rs index 869192aff7b..6f44dcf1912 100644 --- a/libparsec/src/config.rs +++ b/libparsec/src/config.rs @@ -55,12 +55,18 @@ impl From for libparsec_client::ClientConfig { proxy: ProxyConfig::default(), with_monitors: config.with_monitors, prevent_sync_pattern: match config.prevent_sync_pattern { - Some(pattern) => Regex::from_glob_reader( - "client_prevent_sync_pattern", - std::io::Cursor::new(pattern), + Some(custom_glob_ignore) => PreventSyncPattern::from_glob_ignore_file( + &custom_glob_ignore, ) - .expect("Cannot process provided prevent sync pattern file"), - None => Regex::default(), + .unwrap_or_else(|err| { + // Fall back to default pattern if the custom pattern is invalid + log::warn!( + "Invalid custom prevent sync pattern, falling back to default: {}", + err + ); + PreventSyncPattern::default() + }), + None => PreventSyncPattern::default(), }, } }