##// END OF EJS Templates
rust-pathauditor: add Rust implementation of the `pathauditor`...
Raphaël Gomès -
r44737:c18dd48c default
parent child Browse files
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(&current_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(bytes, pos) => format!(
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(bytes, pos) => format!(
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(b"a/b//c".to_vec(), 4)),
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(b"a/b/\0c".to_vec(), 4)),
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