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