]> git.proxmox.com Git - proxmox-backup.git/blame - src/pxar/exclude_pattern.rs
pxar: cleanup: s/PxarDirBuf/PxarDirStack/g and move dir_buffer.rs to dir_stack.rs
[proxmox-backup.git] / src / pxar / exclude_pattern.rs
CommitLineData
cd7dc879
CE
1use std::io::Read;
2use std::ffi::{CStr, CString};
3use std::fs::File;
4use std::os::unix::io::{FromRawFd, RawFd};
5
6use failure::*;
7use libc::{c_char, c_int};
8use nix::fcntl::OFlag;
9use nix::errno::Errno;
10use nix::NixPath;
11use nix::sys::stat::{FileStat, Mode};
12
13pub const FNM_NOMATCH: c_int = 1;
14
15extern "C" {
16 fn fnmatch(pattern: *const c_char, string: *const c_char, flags: c_int) -> c_int;
17}
18
51ac99c3 19#[derive(Debug, PartialEq, Clone, Copy)]
cd7dc879
CE
20pub enum MatchType {
21 None,
22 Exclude,
23 Include,
24 PartialExclude,
25 PartialInclude,
26}
27
28#[derive(Clone)]
29pub struct PxarExcludePattern {
30 pattern: CString,
31 match_exclude: bool,
32 match_dir_only: bool,
33 split_pattern: (CString, CString),
34}
35
36impl PxarExcludePattern {
37 pub fn from_file<P: ?Sized + NixPath>(parent_fd: RawFd, filename: &P) -> Result<Option<(Vec<PxarExcludePattern>, Vec<u8>, FileStat)>, Error> {
38 let stat = match nix::sys::stat::fstatat(parent_fd, filename, nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW) {
39 Ok(stat) => stat,
40 Err(nix::Error::Sys(Errno::ENOENT)) => return Ok(None),
41 Err(err) => bail!("stat failed - {}", err),
42 };
43
44 let filefd = nix::fcntl::openat(parent_fd, filename, OFlag::O_NOFOLLOW, Mode::empty())?;
45 let mut file = unsafe {
46 File::from_raw_fd(filefd)
47 };
48
49 let mut content_buffer = Vec::new();
50 let _bytes = file.read_to_end(&mut content_buffer)?;
51
52 let mut exclude_pattern = Vec::new();
53 for line in content_buffer.split(|&c| c == b'\n') {
54 if line.is_empty() {
55 continue;
56 }
57 if let Some(pattern) = Self::from_line(line)? {
58 exclude_pattern.push(pattern);
59 }
60 }
61
62 Ok(Some((exclude_pattern, content_buffer, stat)))
63 }
64
65 pub fn from_line(line: &[u8]) -> Result<Option<PxarExcludePattern>, Error> {
66 let mut input = line;
67
68 if input.starts_with(b"#") {
69 return Ok(None);
70 }
71
72 let match_exclude = if input.starts_with(b"!") {
73 // Reduce slice view to exclude "!"
74 input = &input[1..];
75 false
76 } else {
77 true
78 };
79
80 // Paths ending in / match only directory names (no filenames)
81 let match_dir_only = if input.ends_with(b"/") {
82 let len = input.len();
83 input = &input[..len - 1];
84 true
85 } else {
86 false
87 };
88
89 // Ignore initial slash
90 if input.starts_with(b"/") {
91 input = &input[1..];
92 }
93
94 if input.is_empty() || input == b"." ||
95 input == b".." || input.contains(&b'\0') {
96 bail!("invalid path component encountered");
97 }
98
99 // This will fail if the line contains b"\0"
100 let pattern = CString::new(input)?;
101 let split_pattern = split_at_slash(&pattern);
102
103 Ok(Some(PxarExcludePattern {
104 pattern,
105 match_exclude,
106 match_dir_only,
107 split_pattern,
108 }))
109 }
110
111 pub fn get_front_pattern(&self) -> PxarExcludePattern {
112 let pattern = split_at_slash(&self.split_pattern.0);
113 PxarExcludePattern {
114 pattern: self.split_pattern.0.clone(),
115 match_exclude: self.match_exclude,
116 match_dir_only: self.match_dir_only,
117 split_pattern: pattern,
118 }
119 }
120
121 pub fn get_rest_pattern(&self) -> PxarExcludePattern {
122 let pattern = split_at_slash(&self.split_pattern.1);
123 PxarExcludePattern {
124 pattern: self.split_pattern.1.clone(),
125 match_exclude: self.match_exclude,
126 match_dir_only: self.match_dir_only,
127 split_pattern: pattern,
128 }
129 }
130
131 pub fn dump(&self) {
132 match (self.match_exclude, self.match_dir_only) {
133 (true, true) => println!("{:#?}/", self.pattern),
134 (true, false) => println!("{:#?}", self.pattern),
135 (false, true) => println!("!{:#?}/", self.pattern),
136 (false, false) => println!("!{:#?}", self.pattern),
137 }
138 }
139
140 pub fn matches_filename(&self, filename: &CStr, is_dir: bool) -> MatchType {
141 let mut res = MatchType::None;
142 let (front, _) = &self.split_pattern;
143
144 let fnmatch_res = unsafe {
145 fnmatch(front.as_ptr() as *const libc::c_char, filename.as_ptr() as *const libc::c_char, 0)
146 };
147 // TODO error cases
148 if fnmatch_res == 0 {
149 res = if self.match_exclude {
150 MatchType::PartialExclude
151 } else {
152 MatchType::PartialInclude
153 };
154 }
155
156 let full = if self.pattern.to_bytes().starts_with(b"**/") {
157 CString::new(&self.pattern.to_bytes()[3..]).unwrap()
158 } else {
159 CString::new(&self.pattern.to_bytes()[..]).unwrap()
160 };
161 let fnmatch_res = unsafe {
162 fnmatch(full.as_ptr() as *const libc::c_char, filename.as_ptr() as *const libc::c_char, 0)
163 };
164 // TODO error cases
165 if fnmatch_res == 0 {
166 res = if self.match_exclude {
167 MatchType::Exclude
168 } else {
169 MatchType::Include
170 };
171 }
172
173 if !is_dir && self.match_dir_only {
174 res = MatchType::None;
175 }
176
a771f907
CE
177 if !is_dir && (res == MatchType::PartialInclude || res == MatchType::PartialExclude) {
178 res = MatchType::None;
179 }
180
cd7dc879
CE
181 res
182 }
183}
184
185fn split_at_slash(match_pattern: &CStr) -> (CString, CString) {
186 let match_pattern = match_pattern.to_bytes();
187
188 let pattern = if match_pattern.starts_with(b"./") {
189 &match_pattern[2..]
190 } else {
191 match_pattern
192 };
193
194 let (mut front, mut rest) = match pattern.iter().position(|&c| c == b'/') {
195 Some(ind) => {
196 let (front, rest) = pattern.split_at(ind);
197 (front, &rest[1..])
198 },
199 None => (pattern, &pattern[0..0]),
200 };
201 // '**' is treated such that it maches any directory
202 if front == b"**" {
203 front = b"*";
204 rest = pattern;
205 }
206
207 // Pattern where valid CStrings before, so it is safe to unwrap the Result
208 let front_pattern = CString::new(front).unwrap();
209 let rest_pattern = CString::new(rest).unwrap();
210 (front_pattern, rest_pattern)
211}