]>
Commit | Line | Data |
---|---|---|
cd7dc879 CE |
1 | use std::io::Read; |
2 | use std::ffi::{CStr, CString}; | |
3 | use std::fs::File; | |
4 | use std::os::unix::io::{FromRawFd, RawFd}; | |
5 | ||
6 | use failure::*; | |
7 | use libc::{c_char, c_int}; | |
8 | use nix::fcntl::OFlag; | |
9 | use nix::errno::Errno; | |
10 | use nix::NixPath; | |
11 | use nix::sys::stat::{FileStat, Mode}; | |
12 | ||
13 | pub const FNM_NOMATCH: c_int = 1; | |
14 | ||
15 | extern "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 |
20 | pub enum MatchType { |
21 | None, | |
22 | Exclude, | |
23 | Include, | |
24 | PartialExclude, | |
25 | PartialInclude, | |
26 | } | |
27 | ||
28 | #[derive(Clone)] | |
29 | pub struct PxarExcludePattern { | |
30 | pattern: CString, | |
31 | match_exclude: bool, | |
32 | match_dir_only: bool, | |
33 | split_pattern: (CString, CString), | |
34 | } | |
35 | ||
36 | impl 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 | ||
185 | fn 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 | } |