Show More
@@ -0,0 +1,230 b'' | |||||
|
1 | // path_auditor.rs | |||
|
2 | // | |||
|
3 | // Copyright 2020 | |||
|
4 | // Raphaël Gomès <rgomes@octobus.net>, | |||
|
5 | // | |||
|
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. | |||
|
8 | ||||
|
9 | use crate::utils::{ | |||
|
10 | files::lower_clean, | |||
|
11 | find_slice_in_slice, | |||
|
12 | hg_path::{hg_path_to_path_buf, HgPath, HgPathBuf, HgPathError}, | |||
|
13 | }; | |||
|
14 | use std::collections::HashSet; | |||
|
15 | use std::path::{Path, PathBuf}; | |||
|
16 | ||||
|
17 | /// Ensures that a path is valid for use in the repository i.e. does not use | |||
|
18 | /// any banned components, does not traverse a symlink, etc. | |||
|
19 | #[derive(Debug, Default)] | |||
|
20 | pub struct PathAuditor { | |||
|
21 | audited: HashSet<HgPathBuf>, | |||
|
22 | audited_dirs: HashSet<HgPathBuf>, | |||
|
23 | root: PathBuf, | |||
|
24 | } | |||
|
25 | ||||
|
26 | impl PathAuditor { | |||
|
27 | pub fn new(root: impl AsRef<Path>) -> Self { | |||
|
28 | Self { | |||
|
29 | root: root.as_ref().to_owned(), | |||
|
30 | ..Default::default() | |||
|
31 | } | |||
|
32 | } | |||
|
33 | pub fn audit_path( | |||
|
34 | &mut self, | |||
|
35 | path: impl AsRef<HgPath>, | |||
|
36 | ) -> Result<(), HgPathError> { | |||
|
37 | // TODO windows "localpath" normalization | |||
|
38 | let path = path.as_ref(); | |||
|
39 | if path.is_empty() { | |||
|
40 | return Ok(()); | |||
|
41 | } | |||
|
42 | // TODO case normalization | |||
|
43 | if self.audited.contains(path) { | |||
|
44 | return Ok(()); | |||
|
45 | } | |||
|
46 | // AIX ignores "/" at end of path, others raise EISDIR. | |||
|
47 | let last_byte = path.as_bytes()[path.len() - 1]; | |||
|
48 | if last_byte == b'/' || last_byte == b'\\' { | |||
|
49 | return Err(HgPathError::EndsWithSlash(path.to_owned())); | |||
|
50 | } | |||
|
51 | let parts: Vec<_> = path | |||
|
52 | .as_bytes() | |||
|
53 | .split(|b| std::path::is_separator(*b as char)) | |||
|
54 | .collect(); | |||
|
55 | ||||
|
56 | let first_component = lower_clean(parts[0]); | |||
|
57 | let first_component = first_component.as_slice(); | |||
|
58 | if !path.split_drive().0.is_empty() | |||
|
59 | || (first_component == b".hg" | |||
|
60 | || first_component == b".hg." | |||
|
61 | || first_component == b"") | |||
|
62 | || parts.iter().any(|c| c == b"..") | |||
|
63 | { | |||
|
64 | return Err(HgPathError::InsideDotHg(path.to_owned())); | |||
|
65 | } | |||
|
66 | ||||
|
67 | // Windows shortname aliases | |||
|
68 | for part in parts.iter() { | |||
|
69 | if part.contains(&b'~') { | |||
|
70 | let mut split = part.splitn(1, |b| *b == b'~'); | |||
|
71 | let first = | |||
|
72 | split.next().unwrap().to_owned().to_ascii_uppercase(); | |||
|
73 | let last = split.next().unwrap(); | |||
|
74 | if last.iter().all(u8::is_ascii_digit) | |||
|
75 | && (first == b"HG" || first == b"HG8B6C") | |||
|
76 | { | |||
|
77 | return Err(HgPathError::ContainsIllegalComponent( | |||
|
78 | path.to_owned(), | |||
|
79 | )); | |||
|
80 | } | |||
|
81 | } | |||
|
82 | } | |||
|
83 | let lower_path = lower_clean(path.as_bytes()); | |||
|
84 | if find_slice_in_slice(&lower_path, b".hg").is_some() { | |||
|
85 | let lower_parts: Vec<_> = path | |||
|
86 | .as_bytes() | |||
|
87 | .split(|b| std::path::is_separator(*b as char)) | |||
|
88 | .collect(); | |||
|
89 | for pattern in [b".hg".to_vec(), b".hg.".to_vec()].iter() { | |||
|
90 | if let Some(pos) = lower_parts[1..] | |||
|
91 | .iter() | |||
|
92 | .position(|part| part == &pattern.as_slice()) | |||
|
93 | { | |||
|
94 | let base = lower_parts[..=pos] | |||
|
95 | .iter() | |||
|
96 | .fold(HgPathBuf::new(), |acc, p| { | |||
|
97 | acc.join(HgPath::new(p)) | |||
|
98 | }); | |||
|
99 | return Err(HgPathError::IsInsideNestedRepo { | |||
|
100 | path: path.to_owned(), | |||
|
101 | nested_repo: base, | |||
|
102 | }); | |||
|
103 | } | |||
|
104 | } | |||
|
105 | } | |||
|
106 | ||||
|
107 | let parts = &parts[..parts.len().saturating_sub(1)]; | |||
|
108 | ||||
|
109 | // We don't want to add "foo/bar/baz" to `audited_dirs` before checking | |||
|
110 | // if there's a "foo/.hg" directory. This also means we won't | |||
|
111 | // accidentally traverse a symlink into some other filesystem (which | |||
|
112 | // is potentially expensive to access). | |||
|
113 | for index in 0..parts.len() { | |||
|
114 | let prefix = &parts[..index + 1].join(&b'/'); | |||
|
115 | let prefix = HgPath::new(prefix); | |||
|
116 | if self.audited_dirs.contains(prefix) { | |||
|
117 | continue; | |||
|
118 | } | |||
|
119 | self.check_filesystem(&prefix, &path)?; | |||
|
120 | } | |||
|
121 | ||||
|
122 | self.audited.insert(path.to_owned()); | |||
|
123 | ||||
|
124 | Ok(()) | |||
|
125 | } | |||
|
126 | ||||
|
127 | pub fn check_filesystem( | |||
|
128 | &self, | |||
|
129 | prefix: impl AsRef<HgPath>, | |||
|
130 | path: impl AsRef<HgPath>, | |||
|
131 | ) -> Result<(), HgPathError> { | |||
|
132 | let prefix = prefix.as_ref(); | |||
|
133 | let path = path.as_ref(); | |||
|
134 | let current_path = self.root.join( | |||
|
135 | hg_path_to_path_buf(prefix) | |||
|
136 | .map_err(|_| HgPathError::NotFsCompliant(path.to_owned()))?, | |||
|
137 | ); | |||
|
138 | match std::fs::symlink_metadata(¤t_path) { | |||
|
139 | Err(e) => { | |||
|
140 | // EINVAL can be raised as invalid path syntax under win32. | |||
|
141 | if e.kind() != std::io::ErrorKind::NotFound | |||
|
142 | && e.kind() != std::io::ErrorKind::InvalidInput | |||
|
143 | && e.raw_os_error() != Some(20) | |||
|
144 | { | |||
|
145 | // Rust does not yet have an `ErrorKind` for | |||
|
146 | // `NotADirectory` (errno 20) | |||
|
147 | // It happens if the dirstate contains `foo/bar` and | |||
|
148 | // foo is not a directory | |||
|
149 | return Err(HgPathError::NotFsCompliant(path.to_owned())); | |||
|
150 | } | |||
|
151 | } | |||
|
152 | Ok(meta) => { | |||
|
153 | if meta.file_type().is_symlink() { | |||
|
154 | return Err(HgPathError::TraversesSymbolicLink { | |||
|
155 | path: path.to_owned(), | |||
|
156 | symlink: prefix.to_owned(), | |||
|
157 | }); | |||
|
158 | } | |||
|
159 | if meta.file_type().is_dir() | |||
|
160 | && current_path.join(".hg").is_dir() | |||
|
161 | { | |||
|
162 | return Err(HgPathError::IsInsideNestedRepo { | |||
|
163 | path: path.to_owned(), | |||
|
164 | nested_repo: prefix.to_owned(), | |||
|
165 | }); | |||
|
166 | } | |||
|
167 | } | |||
|
168 | }; | |||
|
169 | ||||
|
170 | Ok(()) | |||
|
171 | } | |||
|
172 | ||||
|
173 | pub fn check(&mut self, path: impl AsRef<HgPath>) -> bool { | |||
|
174 | self.audit_path(path).is_ok() | |||
|
175 | } | |||
|
176 | } | |||
|
177 | ||||
|
178 | #[cfg(test)] | |||
|
179 | mod tests { | |||
|
180 | use super::*; | |||
|
181 | use crate::utils::files::get_path_from_bytes; | |||
|
182 | use crate::utils::hg_path::path_to_hg_path_buf; | |||
|
183 | ||||
|
184 | #[test] | |||
|
185 | fn test_path_auditor() { | |||
|
186 | let mut auditor = PathAuditor::new(get_path_from_bytes(b"/tmp")); | |||
|
187 | ||||
|
188 | let path = HgPath::new(b".hg/00changelog.i"); | |||
|
189 | assert_eq!( | |||
|
190 | auditor.audit_path(path), | |||
|
191 | Err(HgPathError::InsideDotHg(path.to_owned())) | |||
|
192 | ); | |||
|
193 | let path = HgPath::new(b"this/is/nested/.hg/thing.txt"); | |||
|
194 | assert_eq!( | |||
|
195 | auditor.audit_path(path), | |||
|
196 | Err(HgPathError::IsInsideNestedRepo { | |||
|
197 | path: path.to_owned(), | |||
|
198 | nested_repo: HgPathBuf::from_bytes(b"this/is/nested") | |||
|
199 | }) | |||
|
200 | ); | |||
|
201 | ||||
|
202 | use std::fs::{create_dir, File}; | |||
|
203 | use tempfile::tempdir; | |||
|
204 | ||||
|
205 | let base_dir = tempdir().unwrap(); | |||
|
206 | let base_dir_path = base_dir.path(); | |||
|
207 | let a = base_dir_path.join("a"); | |||
|
208 | let b = base_dir_path.join("b"); | |||
|
209 | create_dir(&a).unwrap(); | |||
|
210 | let in_a_path = a.join("in_a"); | |||
|
211 | File::create(in_a_path).unwrap(); | |||
|
212 | ||||
|
213 | // TODO make portable | |||
|
214 | std::os::unix::fs::symlink(&a, &b).unwrap(); | |||
|
215 | ||||
|
216 | let buf = b.join("in_a").components().skip(2).collect::<PathBuf>(); | |||
|
217 | eprintln!("buf: {}", buf.display()); | |||
|
218 | let path = path_to_hg_path_buf(buf).unwrap(); | |||
|
219 | assert_eq!( | |||
|
220 | auditor.audit_path(&path), | |||
|
221 | Err(HgPathError::TraversesSymbolicLink { | |||
|
222 | path: path, | |||
|
223 | symlink: path_to_hg_path_buf( | |||
|
224 | b.components().skip(2).collect::<PathBuf>() | |||
|
225 | ) | |||
|
226 | .unwrap() | |||
|
227 | }) | |||
|
228 | ); | |||
|
229 | } | |||
|
230 | } |
@@ -146,6 +146,7 b' dependencies = [' | |||||
146 | "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", |
|
146 | "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", | |
147 | "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", |
|
147 | "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |
148 | "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", |
|
148 | "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", | |
|
149 | "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
149 | "twox-hash 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", |
|
150 | "twox-hash 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", | |
150 | ] |
|
151 | ] | |
151 |
|
152 | |||
@@ -393,6 +394,11 b' dependencies = [' | |||||
393 | ] |
|
394 | ] | |
394 |
|
395 | |||
395 | [[package]] |
|
396 | [[package]] | |
|
397 | name = "redox_syscall" | |||
|
398 | version = "0.1.56" | |||
|
399 | source = "registry+https://github.com/rust-lang/crates.io-index" | |||
|
400 | ||||
|
401 | [[package]] | |||
396 | name = "regex" |
|
402 | name = "regex" | |
397 | version = "1.3.3" |
|
403 | version = "1.3.3" | |
398 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
404 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
@@ -409,6 +415,14 b' version = "0.6.13"' | |||||
409 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
415 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
410 |
|
416 | |||
411 | [[package]] |
|
417 | [[package]] | |
|
418 | name = "remove_dir_all" | |||
|
419 | version = "0.5.2" | |||
|
420 | source = "registry+https://github.com/rust-lang/crates.io-index" | |||
|
421 | dependencies = [ | |||
|
422 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
|
423 | ] | |||
|
424 | ||||
|
425 | [[package]] | |||
412 | name = "rustc_version" |
|
426 | name = "rustc_version" | |
413 | version = "0.2.3" |
|
427 | version = "0.2.3" | |
414 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
428 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
@@ -435,6 +449,19 b' version = "0.7.0"' | |||||
435 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
449 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
436 |
|
450 | |||
437 | [[package]] |
|
451 | [[package]] | |
|
452 | name = "tempfile" | |||
|
453 | version = "3.1.0" | |||
|
454 | source = "registry+https://github.com/rust-lang/crates.io-index" | |||
|
455 | dependencies = [ | |||
|
456 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", | |||
|
457 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", | |||
|
458 | "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
|
459 | "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", | |||
|
460 | "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
|
461 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
|
462 | ] | |||
|
463 | ||||
|
464 | [[package]] | |||
438 | name = "thread_local" |
|
465 | name = "thread_local" | |
439 | version = "1.0.1" |
|
466 | version = "1.0.1" | |
440 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
467 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
@@ -519,12 +546,15 b' source = "registry+https://github.com/ru' | |||||
519 | "checksum rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" |
|
546 | "checksum rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" | |
520 | "checksum rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" |
|
547 | "checksum rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" | |
521 | "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" |
|
548 | "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" | |
|
549 | "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" | |||
522 | "checksum regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87" |
|
550 | "checksum regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87" | |
523 | "checksum regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90" |
|
551 | "checksum regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90" | |
|
552 | "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" | |||
524 | "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" |
|
553 | "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" | |
525 | "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" |
|
554 | "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" | |
526 | "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" |
|
555 | "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" | |
527 | "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" |
|
556 | "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" | |
|
557 | "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" | |||
528 | "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" |
|
558 | "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" | |
529 | "checksum twox-hash 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bfd5b7557925ce778ff9b9ef90e3ade34c524b5ff10e239c69a42d546d2af56" |
|
559 | "checksum twox-hash 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bfd5b7557925ce778ff9b9ef90e3ade34c524b5ff10e239c69a42d546d2af56" | |
530 | "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" |
|
560 | "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" |
@@ -18,3 +18,6 b' rand_pcg = "0.1.1"' | |||||
18 | rayon = "1.3.0" |
|
18 | rayon = "1.3.0" | |
19 | regex = "1.1.0" |
|
19 | regex = "1.1.0" | |
20 | twox-hash = "1.5.0" |
|
20 | twox-hash = "1.5.0" | |
|
21 | ||||
|
22 | [dev-dependencies] | |||
|
23 | tempfile = "3.1.0" No newline at end of file |
@@ -9,6 +9,7 b'' | |||||
9 |
|
9 | |||
10 | pub mod files; |
|
10 | pub mod files; | |
11 | pub mod hg_path; |
|
11 | pub mod hg_path; | |
|
12 | pub mod path_auditor; | |||
12 |
|
13 | |||
13 | /// Useful until rust/issues/56345 is stable |
|
14 | /// Useful until rust/issues/56345 is stable | |
14 | /// |
|
15 | /// |
@@ -12,6 +12,8 b'' | |||||
12 | use crate::utils::hg_path::{HgPath, HgPathBuf}; |
|
12 | use crate::utils::hg_path::{HgPath, HgPathBuf}; | |
13 | use std::iter::FusedIterator; |
|
13 | use std::iter::FusedIterator; | |
14 |
|
14 | |||
|
15 | use crate::utils::replace_slice; | |||
|
16 | use lazy_static::lazy_static; | |||
15 | use std::fs::Metadata; |
|
17 | use std::fs::Metadata; | |
16 | use std::path::Path; |
|
18 | use std::path::Path; | |
17 |
|
19 | |||
@@ -85,6 +87,41 b' pub fn normalize_case(path: &HgPath) -> ' | |||||
85 | path.to_ascii_lowercase() |
|
87 | path.to_ascii_lowercase() | |
86 | } |
|
88 | } | |
87 |
|
89 | |||
|
90 | lazy_static! { | |||
|
91 | static ref IGNORED_CHARS: Vec<Vec<u8>> = { | |||
|
92 | [ | |||
|
93 | 0x200c, 0x200d, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d, | |||
|
94 | 0x202e, 0x206a, 0x206b, 0x206c, 0x206d, 0x206e, 0x206f, 0xfeff, | |||
|
95 | ] | |||
|
96 | .iter() | |||
|
97 | .map(|code| { | |||
|
98 | std::char::from_u32(*code) | |||
|
99 | .unwrap() | |||
|
100 | .encode_utf8(&mut [0; 3]) | |||
|
101 | .bytes() | |||
|
102 | .collect() | |||
|
103 | }) | |||
|
104 | .collect() | |||
|
105 | }; | |||
|
106 | } | |||
|
107 | ||||
|
108 | fn hfs_ignore_clean(bytes: &[u8]) -> Vec<u8> { | |||
|
109 | let mut buf = bytes.to_owned(); | |||
|
110 | let needs_escaping = bytes.iter().any(|b| *b == b'\xe2' || *b == b'\xef'); | |||
|
111 | if needs_escaping { | |||
|
112 | for forbidden in IGNORED_CHARS.iter() { | |||
|
113 | replace_slice(&mut buf, forbidden, &[]) | |||
|
114 | } | |||
|
115 | buf | |||
|
116 | } else { | |||
|
117 | buf | |||
|
118 | } | |||
|
119 | } | |||
|
120 | ||||
|
121 | pub fn lower_clean(bytes: &[u8]) -> Vec<u8> { | |||
|
122 | hfs_ignore_clean(&bytes.to_ascii_lowercase()) | |||
|
123 | } | |||
|
124 | ||||
88 | #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] |
|
125 | #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] | |
89 | pub struct HgMetadata { |
|
126 | pub struct HgMetadata { | |
90 | pub st_dev: u64, |
|
127 | pub st_dev: u64, |
@@ -15,12 +15,35 b' use std::path::{Path, PathBuf};' | |||||
15 | pub enum HgPathError { |
|
15 | pub enum HgPathError { | |
16 | /// Bytes from the invalid `HgPath` |
|
16 | /// Bytes from the invalid `HgPath` | |
17 | LeadingSlash(Vec<u8>), |
|
17 | LeadingSlash(Vec<u8>), | |
18 | /// Bytes and index of the second slash |
|
18 | ConsecutiveSlashes { | |
19 | ConsecutiveSlashes(Vec<u8>, usize), |
|
19 | bytes: Vec<u8>, | |
20 | /// Bytes and index of the null byte |
|
20 | second_slash_index: usize, | |
21 | ContainsNullByte(Vec<u8>, usize), |
|
21 | }, | |
|
22 | ContainsNullByte { | |||
|
23 | bytes: Vec<u8>, | |||
|
24 | null_byte_index: usize, | |||
|
25 | }, | |||
22 | /// Bytes |
|
26 | /// Bytes | |
23 | DecodeError(Vec<u8>), |
|
27 | DecodeError(Vec<u8>), | |
|
28 | /// The rest come from audit errors | |||
|
29 | EndsWithSlash(HgPathBuf), | |||
|
30 | ContainsIllegalComponent(HgPathBuf), | |||
|
31 | /// Path is inside the `.hg` folder | |||
|
32 | InsideDotHg(HgPathBuf), | |||
|
33 | IsInsideNestedRepo { | |||
|
34 | path: HgPathBuf, | |||
|
35 | nested_repo: HgPathBuf, | |||
|
36 | }, | |||
|
37 | TraversesSymbolicLink { | |||
|
38 | path: HgPathBuf, | |||
|
39 | symlink: HgPathBuf, | |||
|
40 | }, | |||
|
41 | NotFsCompliant(HgPathBuf), | |||
|
42 | /// `path` is the smallest invalid path | |||
|
43 | NotUnderRoot { | |||
|
44 | path: PathBuf, | |||
|
45 | root: PathBuf, | |||
|
46 | }, | |||
24 | } |
|
47 | } | |
25 |
|
48 | |||
26 | impl ToString for HgPathError { |
|
49 | impl ToString for HgPathError { | |
@@ -29,17 +52,55 b' impl ToString for HgPathError {' | |||||
29 | HgPathError::LeadingSlash(bytes) => { |
|
52 | HgPathError::LeadingSlash(bytes) => { | |
30 | format!("Invalid HgPath '{:?}': has a leading slash.", bytes) |
|
53 | format!("Invalid HgPath '{:?}': has a leading slash.", bytes) | |
31 | } |
|
54 | } | |
32 |
HgPathError::ConsecutiveSlashes |
|
55 | HgPathError::ConsecutiveSlashes { | |
33 | "Invalid HgPath '{:?}': consecutive slahes at pos {}.", |
|
56 | bytes, | |
|
57 | second_slash_index: pos, | |||
|
58 | } => format!( | |||
|
59 | "Invalid HgPath '{:?}': consecutive slashes at pos {}.", | |||
34 | bytes, pos |
|
60 | bytes, pos | |
35 | ), |
|
61 | ), | |
36 |
HgPathError::ContainsNullByte |
|
62 | HgPathError::ContainsNullByte { | |
|
63 | bytes, | |||
|
64 | null_byte_index: pos, | |||
|
65 | } => format!( | |||
37 | "Invalid HgPath '{:?}': contains null byte at pos {}.", |
|
66 | "Invalid HgPath '{:?}': contains null byte at pos {}.", | |
38 | bytes, pos |
|
67 | bytes, pos | |
39 | ), |
|
68 | ), | |
40 | HgPathError::DecodeError(bytes) => { |
|
69 | HgPathError::DecodeError(bytes) => { | |
41 | format!("Invalid HgPath '{:?}': could not be decoded.", bytes) |
|
70 | format!("Invalid HgPath '{:?}': could not be decoded.", bytes) | |
42 | } |
|
71 | } | |
|
72 | HgPathError::EndsWithSlash(path) => { | |||
|
73 | format!("Audit failed for '{}': ends with a slash.", path) | |||
|
74 | } | |||
|
75 | HgPathError::ContainsIllegalComponent(path) => format!( | |||
|
76 | "Audit failed for '{}': contains an illegal component.", | |||
|
77 | path | |||
|
78 | ), | |||
|
79 | HgPathError::InsideDotHg(path) => format!( | |||
|
80 | "Audit failed for '{}': is inside the '.hg' folder.", | |||
|
81 | path | |||
|
82 | ), | |||
|
83 | HgPathError::IsInsideNestedRepo { | |||
|
84 | path, | |||
|
85 | nested_repo: nested, | |||
|
86 | } => format!( | |||
|
87 | "Audit failed for '{}': is inside a nested repository '{}'.", | |||
|
88 | path, nested | |||
|
89 | ), | |||
|
90 | HgPathError::TraversesSymbolicLink { path, symlink } => format!( | |||
|
91 | "Audit failed for '{}': traverses symbolic link '{}'.", | |||
|
92 | path, symlink | |||
|
93 | ), | |||
|
94 | HgPathError::NotFsCompliant(path) => format!( | |||
|
95 | "Audit failed for '{}': cannot be turned into a \ | |||
|
96 | filesystem path.", | |||
|
97 | path | |||
|
98 | ), | |||
|
99 | HgPathError::NotUnderRoot { path, root } => format!( | |||
|
100 | "Audit failed for '{}': not under root {}.", | |||
|
101 | path.display(), | |||
|
102 | root.display() | |||
|
103 | ), | |||
43 | } |
|
104 | } | |
44 | } |
|
105 | } | |
45 | } |
|
106 | } | |
@@ -229,17 +290,17 b' impl HgPath {' | |||||
229 | for (index, byte) in bytes.iter().enumerate() { |
|
290 | for (index, byte) in bytes.iter().enumerate() { | |
230 | match byte { |
|
291 | match byte { | |
231 | 0 => { |
|
292 | 0 => { | |
232 |
return Err(HgPathError::ContainsNullByte |
|
293 | return Err(HgPathError::ContainsNullByte { | |
233 | bytes.to_vec(), |
|
294 | bytes: bytes.to_vec(), | |
234 | index, |
|
295 | null_byte_index: index, | |
235 |
|
|
296 | }) | |
236 | } |
|
297 | } | |
237 | b'/' => { |
|
298 | b'/' => { | |
238 | if previous_byte.is_some() && previous_byte == Some(b'/') { |
|
299 | if previous_byte.is_some() && previous_byte == Some(b'/') { | |
239 |
return Err(HgPathError::ConsecutiveSlashes |
|
300 | return Err(HgPathError::ConsecutiveSlashes { | |
240 | bytes.to_vec(), |
|
301 | bytes: bytes.to_vec(), | |
241 | index, |
|
302 | second_slash_index: index, | |
242 |
|
|
303 | }); | |
243 | } |
|
304 | } | |
244 | } |
|
305 | } | |
245 | _ => (), |
|
306 | _ => (), | |
@@ -431,11 +492,17 b' mod tests {' | |||||
431 | HgPath::new(b"/").check_state() |
|
492 | HgPath::new(b"/").check_state() | |
432 | ); |
|
493 | ); | |
433 | assert_eq!( |
|
494 | assert_eq!( | |
434 |
Err(HgPathError::ConsecutiveSlashes |
|
495 | Err(HgPathError::ConsecutiveSlashes { | |
|
496 | bytes: b"a/b//c".to_vec(), | |||
|
497 | second_slash_index: 4 | |||
|
498 | }), | |||
435 | HgPath::new(b"a/b//c").check_state() |
|
499 | HgPath::new(b"a/b//c").check_state() | |
436 | ); |
|
500 | ); | |
437 | assert_eq!( |
|
501 | assert_eq!( | |
438 |
Err(HgPathError::ContainsNullByte |
|
502 | Err(HgPathError::ContainsNullByte { | |
|
503 | bytes: b"a/b/\0c".to_vec(), | |||
|
504 | null_byte_index: 4 | |||
|
505 | }), | |||
439 | HgPath::new(b"a/b/\0c").check_state() |
|
506 | HgPath::new(b"a/b/\0c").check_state() | |
440 | ); |
|
507 | ); | |
441 | // TODO test HgPathError::DecodeError for the Windows implementation. |
|
508 | // TODO test HgPathError::DecodeError for the Windows implementation. |
General Comments 0
You need to be logged in to leave comments.
Login now