From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 74B1ACC85 for ; Wed, 16 Aug 2023 16:03:38 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 5CE3C16E2B for ; Wed, 16 Aug 2023 16:03:38 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Wed, 16 Aug 2023 16:03:36 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id A7B0D40F8B for ; Wed, 16 Aug 2023 16:03:36 +0200 (CEST) From: Gabriel Goller To: pbs-devel@lists.proxmox.com Date: Wed, 16 Aug 2023 16:03:29 +0200 Message-Id: <20230816140330.191947-1-g.goller@proxmox.com> X-Mailer: git-send-email 2.39.2 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.500 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [PATCH pathpatterns v2] match_list: added `matches_mode_lazy()`, which only retrieves file mode if necessary X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 16 Aug 2023 14:03:38 -0000 Added `matches_mode_lazy()` function, which takes a closure that should return the `file_mode`. The function will go through the patterns and match by path only until it finds a pattern which does not have `MatchFlag::ANY_FILE_TYPE`, in case it will call the closure, which will return a `file_mode`. This will ensure that the closure (which in our case executes `stat()`) will only be run once(at most), every pattern will only be processed once and that the order of the patterns will be respected. Signed-off-by: Gabriel Goller --- changes v2: - updated the `matches_path()` function to take a closure and renamed it to `matches_mode_lazy()`. This allows us to only match once, so we don't need to match before and after `stat()` (without and with `file_mode`) anymore. changes v1: - added `matches_path()` function, which matches by path only and returns an error if the `file_mode` is required. src/match_list.rs | 203 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 1 deletion(-) diff --git a/src/match_list.rs b/src/match_list.rs index c5b14e0..9914130 100644 --- a/src/match_list.rs +++ b/src/match_list.rs @@ -1,6 +1,6 @@ //! Helpers for include/exclude lists. - use bitflags::bitflags; +use std::{error::Error, fmt}; use crate::PatternFlag; @@ -39,6 +39,17 @@ impl Default for MatchFlag { } } +#[derive(Debug, PartialEq)] +pub struct FileModeRequired; + +impl fmt::Display for FileModeRequired { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "File mode is required for matching") + } +} + +impl Error for FileModeRequired {} + /// A pattern entry. (Glob patterns or literal patterns.) // Note: // For regex we'd likely use the POSIX extended REs via `regexec(3)`, since we're targetting @@ -304,12 +315,26 @@ impl MatchEntry { self.matches_path_exact(path) } + + /// Check whether the path contains a matching suffix. Returns an error if a file mode is required. + pub fn matches_path>(&self, path: T) -> Result { + self.matches_path_do(path.as_ref()) + } + + fn matches_path_do(&self, path: &[u8]) -> Result { + if !self.flags.contains(MatchFlag::ANY_FILE_TYPE) { + return Err(FileModeRequired); + } + + Ok(self.matches_path_suffix_do(path)) + } } #[doc(hidden)] pub trait MatchListEntry { fn entry_matches(&self, path: &[u8], file_mode: Option) -> Option; fn entry_matches_exact(&self, path: &[u8], file_mode: Option) -> Option; + fn entry_matches_path(&self, path: &[u8]) -> Result, FileModeRequired>; } impl MatchListEntry for &'_ MatchEntry { @@ -328,6 +353,15 @@ impl MatchListEntry for &'_ MatchEntry { None } } + + fn entry_matches_path(&self, path: &[u8]) -> Result, FileModeRequired> { + let matches = self.matches_path(path)?; + if matches { + Ok(Some(self.match_type())) + } else { + Ok(None) + } + } } impl MatchListEntry for &'_ &'_ MatchEntry { @@ -346,6 +380,15 @@ impl MatchListEntry for &'_ &'_ MatchEntry { None } } + + fn entry_matches_path(&self, path: &[u8]) -> Result, FileModeRequired> { + let matches = self.matches_path(path)?; + if matches { + Ok(Some(self.match_type())) + } else { + Ok(None) + } + } } /// This provides [`matches`](MatchList::matches) and [`matches_exact`](MatchList::matches_exact) @@ -374,6 +417,22 @@ pub trait MatchList { } fn matches_exact_do(&self, path: &[u8], file_mode: Option) -> Option; + + /// Check whether this list contains anything exactly matching the path, returns error if + /// `file_mode` is required for exact matching and cannot be retrieved using closure. + fn matches_mode_lazy, E: Error, U: FnMut() -> Result>( + &self, + path: T, + get_file_mode: U, + ) -> Result, E> { + self.matches_mode_lazy_do(path.as_ref(), get_file_mode) + } + + fn matches_mode_lazy_do Result>( + &self, + path: &[u8], + get_file_mode: T, + ) -> Result, E>; } impl<'a, T> MatchList for T @@ -408,6 +467,39 @@ where None } + + fn matches_mode_lazy_do Result>( + &self, + path: &[u8], + mut get_file_mode: U, + ) -> Result, E> { + // This is an &self method on a `T where T: 'a`. + let this: &'a Self = unsafe { std::mem::transmute(self) }; + + let mut file_mode: Option = None; + for m in this.into_iter().rev() { + if file_mode.is_none() { + match m.entry_matches_path(path) { + Ok(mt) => { + if mt.is_some() { + return Ok(mt); + } + } + Err(_) => match get_file_mode() { + Ok(x) => file_mode = Some(x), + Err(e) => return Err(e), + }, + } + } + if file_mode.is_some() { + let full_match = m.entry_matches(path, file_mode); + if full_match.is_some() { + return Ok(full_match); + } + } + } + Ok(None) + } } #[test] @@ -530,3 +622,112 @@ fn test_path_relativity() { assert_eq!(matchlist.matches("foo/slash", None), None); assert_eq!(matchlist.matches("foo/slash-a", None), None); } + +#[test] +fn matches_path() { + use crate::Pattern; + + #[derive(fmt::Debug, PartialEq)] + struct ExampleErr {} + impl fmt::Display for ExampleErr { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + todo!() + } + } + impl Error for ExampleErr {} + + let matchlist = vec![ + MatchEntry::new(Pattern::path("a*").unwrap(), MatchType::Exclude), + MatchEntry::new(Pattern::path("b*").unwrap(), MatchType::Exclude), + ]; + + assert_eq!( + matchlist.matches_mode_lazy("ahsjdj", || Err(ExampleErr {})), + Ok(Some(MatchType::Exclude)) + ); + let mut _test = MatchFlag::ANY_FILE_TYPE; + assert_eq!( + matchlist.matches_mode_lazy("bhshdf", || { + _test = MatchFlag::MATCH_DIRECTORIES; + Ok::(libc::S_IFDIR) + }), + Ok(Some(MatchType::Exclude)) + ); + + let matchlist = vec![ + MatchEntry::new(Pattern::path("a*").unwrap(), MatchType::Exclude) + .flags(MatchFlag::MATCH_DIRECTORIES), + MatchEntry::new(Pattern::path("b*").unwrap(), MatchType::Exclude), + ]; + + assert_eq!( + matchlist.matches_mode_lazy("ahsjdj", || Err(ExampleErr {})), + Err(ExampleErr {}) + ); + assert_eq!( + matchlist.matches_mode_lazy("bhshdf", || Err(ExampleErr {})), + Ok(Some(MatchType::Exclude)) + ); + assert_eq!( + matchlist.matches_mode_lazy("ahsjdj", || Ok::(libc::S_IFDIR)), + Ok(Some(MatchType::Exclude)) + ); + + let matchlist = vec![ + MatchEntry::new(Pattern::path("b*").unwrap(), MatchType::Include), + MatchEntry::new(Pattern::path("a*").unwrap(), MatchType::Exclude) + .flags(MatchFlag::MATCH_DIRECTORIES), + ]; + + assert_eq!( + matchlist.matches_mode_lazy("ahsjdj", || Err(ExampleErr {})), + Err(ExampleErr {}) + ); + assert_eq!( + matchlist.matches_mode_lazy("bhshdf", || Err(ExampleErr {})), + Err(ExampleErr {}) + ); + + assert_eq!( + matchlist.matches_mode_lazy("ahsjdj", || Ok::(libc::S_IFDIR)), + Ok(Some(MatchType::Exclude)) + ); + assert_eq!( + matchlist.matches_mode_lazy("bhshdf", || Err(ExampleErr {})), + Err(ExampleErr {}) + ); + assert_eq!( + matchlist.matches_mode_lazy("bbb", || Ok::(libc::S_IFDIR)), + Ok(Some(MatchType::Include)) + ); + + let matchlist = vec![ + MatchEntry::new(Pattern::path("a*").unwrap(), MatchType::Exclude) + .flags(MatchFlag::MATCH_DIRECTORIES), + ]; + + assert_eq!( + matchlist.matches_mode_lazy("bbb", || Ok::(libc::S_IFDIR)), + Ok(None) + ); + + let matchlist = vec![ + MatchEntry::new(Pattern::path("a*").unwrap(), MatchType::Exclude) + .flags(MatchFlag::MATCH_DIRECTORIES), + MatchEntry::new(Pattern::path("b*").unwrap(), MatchType::Exclude) + .flags(MatchFlag::MATCH_REGULAR_FILES), + ]; + + assert_eq!( + matchlist.matches_mode_lazy("ahsjdj", || Ok::(libc::S_IFDIR)), + Ok(Some(MatchType::Exclude)) + ); + assert_eq!( + matchlist.matches_mode_lazy("ahsjdj", || Ok::(libc::S_IFREG)), + Ok(None) + ); + assert_eq!( + matchlist.matches_mode_lazy("bhsjdj", || Ok::(libc::S_IFREG)), + Ok(Some(MatchType::Exclude)) + ); +} -- 2.39.2