]>
Commit | Line | Data |
---|---|---|
eecb1828 CE |
1 | //! `MatchPattern` defines a match pattern used to match filenames encountered |
2 | //! during encoding or decoding of a `pxar` archive. | |
3 | //! `fnmatch` is used internally to match filenames against the patterns. | |
4 | //! Shell wildcard pattern can be used to match multiple filenames, see manpage | |
5 | //! `glob(7)`. | |
6 | //! `**` is treated special, as it matches multiple directories in a path. | |
7 | ||
cd7dc879 CE |
8 | use std::ffi::{CStr, CString}; |
9 | use std::fs::File; | |
d792dc3c | 10 | use std::io::Read; |
cd7dc879 CE |
11 | use std::os::unix::io::{FromRawFd, RawFd}; |
12 | ||
d792dc3c | 13 | use failure::{bail, Error}; |
cd7dc879 | 14 | use libc::{c_char, c_int}; |
d792dc3c | 15 | use nix::errno::Errno; |
4d142ea7 CE |
16 | use nix::fcntl; |
17 | use nix::fcntl::{AtFlags, OFlag}; | |
4d142ea7 | 18 | use nix::sys::stat; |
cd7dc879 | 19 | use nix::sys::stat::{FileStat, Mode}; |
d792dc3c | 20 | use nix::NixPath; |
cd7dc879 | 21 | |
4d142ea7 | 22 | pub const FNM_NOMATCH: c_int = 1; |
cd7dc879 CE |
23 | |
24 | extern "C" { | |
25 | fn fnmatch(pattern: *const c_char, string: *const c_char, flags: c_int) -> c_int; | |
26 | } | |
27 | ||
51ac99c3 | 28 | #[derive(Debug, PartialEq, Clone, Copy)] |
cd7dc879 CE |
29 | pub enum MatchType { |
30 | None, | |
4d142ea7 CE |
31 | Positive, |
32 | Negative, | |
33 | PartialPositive, | |
34 | PartialNegative, | |
cd7dc879 CE |
35 | } |
36 | ||
eecb1828 CE |
37 | /// `MatchPattern` provides functionality for filename glob pattern matching |
38 | /// based on glibc's `fnmatch`. | |
39 | /// Positive matches return `MatchType::PartialPositive` or `MatchType::Positive`. | |
40 | /// Patterns starting with `!` are interpreted as negation, meaning they will | |
41 | /// return `MatchType::PartialNegative` or `MatchType::Negative`. | |
42 | /// No matches result in `MatchType::None`. | |
43 | /// # Examples: | |
44 | /// ``` | |
45 | /// # use std::ffi::CString; | |
46 | /// # use self::proxmox_backup::pxar::{MatchPattern, MatchType}; | |
47 | /// # fn main() -> Result<(), failure::Error> { | |
48 | /// let filename = CString::new("some.conf")?; | |
49 | /// let is_dir = false; | |
50 | /// | |
51 | /// /// Positive match of any file ending in `.conf` in any subdirectory | |
52 | /// let positive = MatchPattern::from_line(b"**/*.conf")?.unwrap(); | |
53 | /// let m_positive = positive.matches_filename(&filename, is_dir)?; | |
54 | /// assert!(m_positive == MatchType::Positive); | |
55 | /// | |
56 | /// /// Negative match of filenames starting with `s` | |
57 | /// let negative = MatchPattern::from_line(b"![s]*")?.unwrap(); | |
58 | /// let m_negative = negative.matches_filename(&filename, is_dir)?; | |
59 | /// assert!(m_negative == MatchType::Negative); | |
60 | /// # Ok(()) | |
61 | /// # } | |
62 | /// ``` | |
cd7dc879 | 63 | #[derive(Clone)] |
4d142ea7 | 64 | pub struct MatchPattern { |
cd7dc879 | 65 | pattern: CString, |
4d142ea7 | 66 | match_positive: bool, |
cd7dc879 CE |
67 | match_dir_only: bool, |
68 | split_pattern: (CString, CString), | |
69 | } | |
70 | ||
4d142ea7 | 71 | impl MatchPattern { |
eecb1828 CE |
72 | /// Read a list of `MatchPattern` from file. |
73 | /// The file is read line by line (lines terminated by newline character), | |
74 | /// each line may only contain one pattern. | |
75 | /// Leading `/` are ignored and lines starting with `#` are interpreted as | |
76 | /// comments and not included in the resulting list. | |
77 | /// Patterns ending in `/` will match only directories. | |
78 | /// | |
79 | /// On success, a list of match pattern is returned as well as the raw file | |
80 | /// byte buffer together with the files stats. | |
81 | /// This is done in order to avoid reading the file more than once during | |
82 | /// encoding of the archive. | |
4d142ea7 CE |
83 | pub fn from_file<P: ?Sized + NixPath>( |
84 | parent_fd: RawFd, | |
85 | filename: &P, | |
86 | ) -> Result<Option<(Vec<MatchPattern>, Vec<u8>, FileStat)>, Error> { | |
4d142ea7 | 87 | let stat = match stat::fstatat(parent_fd, filename, AtFlags::AT_SYMLINK_NOFOLLOW) { |
cd7dc879 CE |
88 | Ok(stat) => stat, |
89 | Err(nix::Error::Sys(Errno::ENOENT)) => return Ok(None), | |
90 | Err(err) => bail!("stat failed - {}", err), | |
91 | }; | |
92 | ||
4d142ea7 | 93 | let filefd = fcntl::openat(parent_fd, filename, OFlag::O_NOFOLLOW, Mode::empty())?; |
d792dc3c | 94 | let mut file = unsafe { File::from_raw_fd(filefd) }; |
cd7dc879 CE |
95 | |
96 | let mut content_buffer = Vec::new(); | |
97 | let _bytes = file.read_to_end(&mut content_buffer)?; | |
98 | ||
4d142ea7 | 99 | let mut match_pattern = Vec::new(); |
cd7dc879 CE |
100 | for line in content_buffer.split(|&c| c == b'\n') { |
101 | if line.is_empty() { | |
102 | continue; | |
103 | } | |
104 | if let Some(pattern) = Self::from_line(line)? { | |
4d142ea7 | 105 | match_pattern.push(pattern); |
cd7dc879 CE |
106 | } |
107 | } | |
108 | ||
4d142ea7 | 109 | Ok(Some((match_pattern, content_buffer, stat))) |
cd7dc879 CE |
110 | } |
111 | ||
eecb1828 CE |
112 | /// Interprete a byte buffer as a sinlge line containing a valid |
113 | /// `MatchPattern`. | |
114 | /// Pattern starting with `#` are interpreted as comments, returning `Ok(None)`. | |
115 | /// Pattern starting with '!' are interpreted as negative match pattern. | |
116 | /// Pattern with trailing `/` match only against directories. | |
117 | /// `.` as well as `..` and any pattern containing `\0` are invalid and will | |
118 | /// result in an error. | |
4d142ea7 | 119 | pub fn from_line(line: &[u8]) -> Result<Option<MatchPattern>, Error> { |
cd7dc879 CE |
120 | let mut input = line; |
121 | ||
122 | if input.starts_with(b"#") { | |
123 | return Ok(None); | |
124 | } | |
125 | ||
4d142ea7 | 126 | let match_positive = if input.starts_with(b"!") { |
cd7dc879 CE |
127 | // Reduce slice view to exclude "!" |
128 | input = &input[1..]; | |
129 | false | |
130 | } else { | |
131 | true | |
132 | }; | |
133 | ||
134 | // Paths ending in / match only directory names (no filenames) | |
135 | let match_dir_only = if input.ends_with(b"/") { | |
136 | let len = input.len(); | |
137 | input = &input[..len - 1]; | |
138 | true | |
139 | } else { | |
140 | false | |
141 | }; | |
142 | ||
143 | // Ignore initial slash | |
144 | if input.starts_with(b"/") { | |
145 | input = &input[1..]; | |
146 | } | |
147 | ||
d792dc3c | 148 | if input.is_empty() || input == b"." || input == b".." || input.contains(&b'\0') { |
cd7dc879 CE |
149 | bail!("invalid path component encountered"); |
150 | } | |
151 | ||
152 | // This will fail if the line contains b"\0" | |
153 | let pattern = CString::new(input)?; | |
154 | let split_pattern = split_at_slash(&pattern); | |
155 | ||
4d142ea7 | 156 | Ok(Some(MatchPattern { |
cd7dc879 | 157 | pattern, |
4d142ea7 | 158 | match_positive, |
cd7dc879 CE |
159 | match_dir_only, |
160 | split_pattern, | |
161 | })) | |
162 | } | |
163 | ||
eecb1828 CE |
164 | /// Returns the pattern before the first `/` encountered as `MatchPattern`. |
165 | /// If no slash is encountered, the `MatchPattern` will be a copy of the | |
166 | /// original pattern. | |
167 | /// ``` | |
168 | /// # use self::proxmox_backup::pxar::{MatchPattern, MatchType}; | |
169 | /// # fn main() -> Result<(), failure::Error> { | |
170 | /// let pattern = MatchPattern::from_line(b"some/match/pattern/")?.unwrap(); | |
171 | /// let front = pattern.get_front_pattern(); | |
172 | /// /// ... will be the same as ... | |
173 | /// let front_pattern = MatchPattern::from_line(b"some")?.unwrap(); | |
174 | /// # Ok(()) | |
175 | /// # } | |
176 | /// ``` | |
4d142ea7 | 177 | pub fn get_front_pattern(&self) -> MatchPattern { |
cd7dc879 | 178 | let pattern = split_at_slash(&self.split_pattern.0); |
4d142ea7 | 179 | MatchPattern { |
cd7dc879 | 180 | pattern: self.split_pattern.0.clone(), |
4d142ea7 | 181 | match_positive: self.match_positive, |
cd7dc879 CE |
182 | match_dir_only: self.match_dir_only, |
183 | split_pattern: pattern, | |
184 | } | |
185 | } | |
186 | ||
eecb1828 CE |
187 | /// Returns the pattern after the first encountered `/` as `MatchPattern`. |
188 | /// If no slash is encountered, the `MatchPattern` will be empty. | |
189 | /// ``` | |
190 | /// # use self::proxmox_backup::pxar::{MatchPattern, MatchType}; | |
191 | /// # fn main() -> Result<(), failure::Error> { | |
192 | /// let pattern = MatchPattern::from_line(b"some/match/pattern/")?.unwrap(); | |
193 | /// let rest = pattern.get_rest_pattern(); | |
194 | /// /// ... will be the same as ... | |
195 | /// let rest_pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap(); | |
196 | /// # Ok(()) | |
197 | /// # } | |
198 | /// ``` | |
4d142ea7 | 199 | pub fn get_rest_pattern(&self) -> MatchPattern { |
cd7dc879 | 200 | let pattern = split_at_slash(&self.split_pattern.1); |
4d142ea7 | 201 | MatchPattern { |
cd7dc879 | 202 | pattern: self.split_pattern.1.clone(), |
4d142ea7 | 203 | match_positive: self.match_positive, |
cd7dc879 CE |
204 | match_dir_only: self.match_dir_only, |
205 | split_pattern: pattern, | |
206 | } | |
207 | } | |
208 | ||
eecb1828 CE |
209 | /// Dump the content of the `MatchPattern` to stdout. |
210 | /// Intended for debugging purposes only. | |
cd7dc879 | 211 | pub fn dump(&self) { |
4d142ea7 | 212 | match (self.match_positive, self.match_dir_only) { |
cd7dc879 CE |
213 | (true, true) => println!("{:#?}/", self.pattern), |
214 | (true, false) => println!("{:#?}", self.pattern), | |
215 | (false, true) => println!("!{:#?}/", self.pattern), | |
216 | (false, false) => println!("!{:#?}", self.pattern), | |
217 | } | |
218 | } | |
219 | ||
e50a90e0 CE |
220 | /// Convert a list of MatchPattern to bytes in order to write them to e.g. |
221 | /// a file. | |
222 | pub fn to_bytes(patterns: &[MatchPattern]) -> Vec<u8> { | |
223 | let mut buffer = Vec::new(); | |
224 | for pattern in patterns { | |
920243b1 DM |
225 | if !pattern.match_positive { buffer.push(b'!'); } |
226 | buffer.extend_from_slice( pattern.pattern.as_bytes()); | |
227 | if pattern.match_dir_only { buffer.push(b'/'); } | |
e50a90e0 CE |
228 | buffer.push(b'\n'); |
229 | } | |
230 | buffer | |
231 | } | |
232 | ||
eecb1828 CE |
233 | /// Match the given filename against this `MatchPattern`. |
234 | /// If the filename matches the pattern completely, `MatchType::Positive` or | |
235 | /// `MatchType::Negative` is returned, depending if the match pattern is was | |
236 | /// declared as positive (no `!` prefix) or negative (`!` prefix). | |
237 | /// If the pattern matched only up to the first slash of the pattern, | |
238 | /// `MatchType::PartialPositive` or `MatchType::PartialNegatie` is returned. | |
239 | /// If the pattern was postfixed by a trailing `/` a match is only valid if | |
240 | /// the parameter `is_dir` equals `true`. | |
241 | /// No match results in `MatchType::None`. | |
43e892d2 | 242 | pub fn matches_filename(&self, filename: &CStr, is_dir: bool) -> Result<MatchType, Error> { |
cd7dc879 CE |
243 | let mut res = MatchType::None; |
244 | let (front, _) = &self.split_pattern; | |
245 | ||
246 | let fnmatch_res = unsafe { | |
4d142ea7 CE |
247 | let front_ptr = front.as_ptr() as *const libc::c_char; |
248 | let filename_ptr = filename.as_ptr() as *const libc::c_char; | |
d792dc3c | 249 | fnmatch(front_ptr, filename_ptr, 0) |
cd7dc879 | 250 | }; |
43e892d2 CE |
251 | if fnmatch_res < 0 { |
252 | bail!("error in fnmatch inside of MatchPattern"); | |
253 | } | |
cd7dc879 | 254 | if fnmatch_res == 0 { |
4d142ea7 CE |
255 | res = if self.match_positive { |
256 | MatchType::PartialPositive | |
cd7dc879 | 257 | } else { |
4d142ea7 | 258 | MatchType::PartialNegative |
cd7dc879 CE |
259 | }; |
260 | } | |
261 | ||
262 | let full = if self.pattern.to_bytes().starts_with(b"**/") { | |
263 | CString::new(&self.pattern.to_bytes()[3..]).unwrap() | |
264 | } else { | |
265 | CString::new(&self.pattern.to_bytes()[..]).unwrap() | |
266 | }; | |
267 | let fnmatch_res = unsafe { | |
4d142ea7 CE |
268 | let full_ptr = full.as_ptr() as *const libc::c_char; |
269 | let filename_ptr = filename.as_ptr() as *const libc::c_char; | |
270 | fnmatch(full_ptr, filename_ptr, 0) | |
cd7dc879 | 271 | }; |
43e892d2 CE |
272 | if fnmatch_res < 0 { |
273 | bail!("error in fnmatch inside of MatchPattern"); | |
274 | } | |
cd7dc879 | 275 | if fnmatch_res == 0 { |
4d142ea7 CE |
276 | res = if self.match_positive { |
277 | MatchType::Positive | |
cd7dc879 | 278 | } else { |
4d142ea7 | 279 | MatchType::Negative |
cd7dc879 CE |
280 | }; |
281 | } | |
282 | ||
283 | if !is_dir && self.match_dir_only { | |
284 | res = MatchType::None; | |
285 | } | |
286 | ||
4d142ea7 | 287 | if !is_dir && (res == MatchType::PartialPositive || res == MatchType::PartialNegative) { |
a771f907 CE |
288 | res = MatchType::None; |
289 | } | |
290 | ||
43e892d2 | 291 | Ok(res) |
cd7dc879 CE |
292 | } |
293 | } | |
294 | ||
eecb1828 CE |
295 | // Splits the `CStr` slice at the first slash encountered and returns the |
296 | // content before (front pattern) and after the slash (rest pattern), | |
297 | // omitting the slash itself. | |
298 | // Slices starting with `**/` are an exception to this, as the corresponding | |
299 | // `MatchPattern` is intended to match multiple directories. | |
300 | // These pattern slices therefore return a `*` as front pattern and the original | |
301 | // pattern itself as rest pattern. | |
cd7dc879 CE |
302 | fn split_at_slash(match_pattern: &CStr) -> (CString, CString) { |
303 | let match_pattern = match_pattern.to_bytes(); | |
304 | ||
305 | let pattern = if match_pattern.starts_with(b"./") { | |
306 | &match_pattern[2..] | |
307 | } else { | |
308 | match_pattern | |
309 | }; | |
310 | ||
311 | let (mut front, mut rest) = match pattern.iter().position(|&c| c == b'/') { | |
312 | Some(ind) => { | |
313 | let (front, rest) = pattern.split_at(ind); | |
314 | (front, &rest[1..]) | |
d792dc3c | 315 | } |
cd7dc879 CE |
316 | None => (pattern, &pattern[0..0]), |
317 | }; | |
318 | // '**' is treated such that it maches any directory | |
319 | if front == b"**" { | |
320 | front = b"*"; | |
321 | rest = pattern; | |
322 | } | |
323 | ||
324 | // Pattern where valid CStrings before, so it is safe to unwrap the Result | |
325 | let front_pattern = CString::new(front).unwrap(); | |
326 | let rest_pattern = CString::new(rest).unwrap(); | |
327 | (front_pattern, rest_pattern) | |
328 | } |