##// END OF EJS Templates
rust: fix testing with $TMPDIR ≠ /tmp...
Dan Villiom Podlaski Christiansen -
r46862:db2bc9e6 default
parent child Browse files
Show More
@@ -1,232 +1,233 b''
1 1 // path_auditor.rs
2 2 //
3 3 // Copyright 2020
4 4 // Raphaël Gomès <rgomes@octobus.net>,
5 5 //
6 6 // This software may be used and distributed according to the terms of the
7 7 // GNU General Public License version 2 or any later version.
8 8
9 9 use crate::utils::{
10 10 files::lower_clean,
11 11 find_slice_in_slice,
12 12 hg_path::{hg_path_to_path_buf, HgPath, HgPathBuf, HgPathError},
13 13 };
14 14 use std::collections::HashSet;
15 15 use std::path::{Path, PathBuf};
16 16 use std::sync::{Mutex, RwLock};
17 17
18 18 /// Ensures that a path is valid for use in the repository i.e. does not use
19 19 /// any banned components, does not traverse a symlink, etc.
20 20 #[derive(Debug, Default)]
21 21 pub struct PathAuditor {
22 22 audited: Mutex<HashSet<HgPathBuf>>,
23 23 audited_dirs: RwLock<HashSet<HgPathBuf>>,
24 24 root: PathBuf,
25 25 }
26 26
27 27 impl PathAuditor {
28 28 pub fn new(root: impl AsRef<Path>) -> Self {
29 29 Self {
30 30 root: root.as_ref().to_owned(),
31 31 ..Default::default()
32 32 }
33 33 }
34 34 pub fn audit_path(
35 35 &self,
36 36 path: impl AsRef<HgPath>,
37 37 ) -> Result<(), HgPathError> {
38 38 // TODO windows "localpath" normalization
39 39 let path = path.as_ref();
40 40 if path.is_empty() {
41 41 return Ok(());
42 42 }
43 43 // TODO case normalization
44 44 if self.audited.lock().unwrap().contains(path) {
45 45 return Ok(());
46 46 }
47 47 // AIX ignores "/" at end of path, others raise EISDIR.
48 48 let last_byte = path.as_bytes()[path.len() - 1];
49 49 if last_byte == b'/' || last_byte == b'\\' {
50 50 return Err(HgPathError::EndsWithSlash(path.to_owned()));
51 51 }
52 52 let parts: Vec<_> = path
53 53 .as_bytes()
54 54 .split(|b| std::path::is_separator(*b as char))
55 55 .collect();
56 56
57 57 let first_component = lower_clean(parts[0]);
58 58 let first_component = first_component.as_slice();
59 59 if !path.split_drive().0.is_empty()
60 60 || (first_component == b".hg"
61 61 || first_component == b".hg."
62 62 || first_component == b"")
63 63 || parts.iter().any(|c| c == b"..")
64 64 {
65 65 return Err(HgPathError::InsideDotHg(path.to_owned()));
66 66 }
67 67
68 68 // Windows shortname aliases
69 69 for part in parts.iter() {
70 70 if part.contains(&b'~') {
71 71 let mut split = part.splitn(2, |b| *b == b'~');
72 72 let first =
73 73 split.next().unwrap().to_owned().to_ascii_uppercase();
74 74 let last = split.next().unwrap();
75 75 if last.iter().all(u8::is_ascii_digit)
76 76 && (first == b"HG" || first == b"HG8B6C")
77 77 {
78 78 return Err(HgPathError::ContainsIllegalComponent(
79 79 path.to_owned(),
80 80 ));
81 81 }
82 82 }
83 83 }
84 84 let lower_path = lower_clean(path.as_bytes());
85 85 if find_slice_in_slice(&lower_path, b".hg").is_some() {
86 86 let lower_parts: Vec<_> = path
87 87 .as_bytes()
88 88 .split(|b| std::path::is_separator(*b as char))
89 89 .collect();
90 90 for pattern in [b".hg".to_vec(), b".hg.".to_vec()].iter() {
91 91 if let Some(pos) = lower_parts[1..]
92 92 .iter()
93 93 .position(|part| part == &pattern.as_slice())
94 94 {
95 95 let base = lower_parts[..=pos]
96 96 .iter()
97 97 .fold(HgPathBuf::new(), |acc, p| {
98 98 acc.join(HgPath::new(p))
99 99 });
100 100 return Err(HgPathError::IsInsideNestedRepo {
101 101 path: path.to_owned(),
102 102 nested_repo: base,
103 103 });
104 104 }
105 105 }
106 106 }
107 107
108 108 let parts = &parts[..parts.len().saturating_sub(1)];
109 109
110 110 // We don't want to add "foo/bar/baz" to `audited_dirs` before checking
111 111 // if there's a "foo/.hg" directory. This also means we won't
112 112 // accidentally traverse a symlink into some other filesystem (which
113 113 // is potentially expensive to access).
114 114 for index in 0..parts.len() {
115 115 let prefix = &parts[..=index].join(&b'/');
116 116 let prefix = HgPath::new(prefix);
117 117 if self.audited_dirs.read().unwrap().contains(prefix) {
118 118 continue;
119 119 }
120 120 self.check_filesystem(&prefix, &path)?;
121 121 self.audited_dirs.write().unwrap().insert(prefix.to_owned());
122 122 }
123 123
124 124 self.audited.lock().unwrap().insert(path.to_owned());
125 125
126 126 Ok(())
127 127 }
128 128
129 129 pub fn check_filesystem(
130 130 &self,
131 131 prefix: impl AsRef<HgPath>,
132 132 path: impl AsRef<HgPath>,
133 133 ) -> Result<(), HgPathError> {
134 134 let prefix = prefix.as_ref();
135 135 let path = path.as_ref();
136 136 let current_path = self.root.join(
137 137 hg_path_to_path_buf(prefix)
138 138 .map_err(|_| HgPathError::NotFsCompliant(path.to_owned()))?,
139 139 );
140 140 match std::fs::symlink_metadata(&current_path) {
141 141 Err(e) => {
142 142 // EINVAL can be raised as invalid path syntax under win32.
143 143 if e.kind() != std::io::ErrorKind::NotFound
144 144 && e.kind() != std::io::ErrorKind::InvalidInput
145 145 && e.raw_os_error() != Some(20)
146 146 {
147 147 // Rust does not yet have an `ErrorKind` for
148 148 // `NotADirectory` (errno 20)
149 149 // It happens if the dirstate contains `foo/bar` and
150 150 // foo is not a directory
151 151 return Err(HgPathError::NotFsCompliant(path.to_owned()));
152 152 }
153 153 }
154 154 Ok(meta) => {
155 155 if meta.file_type().is_symlink() {
156 156 return Err(HgPathError::TraversesSymbolicLink {
157 157 path: path.to_owned(),
158 158 symlink: prefix.to_owned(),
159 159 });
160 160 }
161 161 if meta.file_type().is_dir()
162 162 && current_path.join(".hg").is_dir()
163 163 {
164 164 return Err(HgPathError::IsInsideNestedRepo {
165 165 path: path.to_owned(),
166 166 nested_repo: prefix.to_owned(),
167 167 });
168 168 }
169 169 }
170 170 };
171 171
172 172 Ok(())
173 173 }
174 174
175 175 pub fn check(&self, path: impl AsRef<HgPath>) -> bool {
176 176 self.audit_path(path).is_ok()
177 177 }
178 178 }
179 179
180 180 #[cfg(test)]
181 181 mod tests {
182 182 use super::*;
183 183 use crate::utils::files::get_path_from_bytes;
184 184 use crate::utils::hg_path::path_to_hg_path_buf;
185 185
186 186 #[test]
187 187 fn test_path_auditor() {
188 188 let auditor = PathAuditor::new(get_path_from_bytes(b"/tmp"));
189 189
190 190 let path = HgPath::new(b".hg/00changelog.i");
191 191 assert_eq!(
192 192 auditor.audit_path(path),
193 193 Err(HgPathError::InsideDotHg(path.to_owned()))
194 194 );
195 195 let path = HgPath::new(b"this/is/nested/.hg/thing.txt");
196 196 assert_eq!(
197 197 auditor.audit_path(path),
198 198 Err(HgPathError::IsInsideNestedRepo {
199 199 path: path.to_owned(),
200 200 nested_repo: HgPathBuf::from_bytes(b"this/is/nested")
201 201 })
202 202 );
203 203
204 204 use std::fs::{create_dir, File};
205 205 use tempfile::tempdir;
206 206
207 207 let base_dir = tempdir().unwrap();
208 208 let base_dir_path = base_dir.path();
209 let skip = base_dir_path.components().count() - 1;
209 210 let a = base_dir_path.join("a");
210 211 let b = base_dir_path.join("b");
211 212 create_dir(&a).unwrap();
212 213 let in_a_path = a.join("in_a");
213 214 File::create(in_a_path).unwrap();
214 215
215 216 // TODO make portable
216 217 std::os::unix::fs::symlink(&a, &b).unwrap();
217 218
218 let buf = b.join("in_a").components().skip(2).collect::<PathBuf>();
219 let buf = b.join("in_a").components().skip(skip).collect::<PathBuf>();
219 220 eprintln!("buf: {}", buf.display());
220 221 let path = path_to_hg_path_buf(buf).unwrap();
221 222 assert_eq!(
222 223 auditor.audit_path(&path),
223 224 Err(HgPathError::TraversesSymbolicLink {
224 225 path: path,
225 226 symlink: path_to_hg_path_buf(
226 227 b.components().skip(2).collect::<PathBuf>()
227 228 )
228 229 .unwrap()
229 230 })
230 231 );
231 232 }
232 233 }
General Comments 0
You need to be logged in to leave comments. Login now