1use std::{
13 borrow::{Borrow, Cow},
14 cmp::Ordering,
15 collections::VecDeque,
16 ffi::{CStr, OsStr, OsString},
17 ops::Deref,
18 os::{
19 fd::RawFd,
20 unix::ffi::{OsStrExt, OsStringExt},
21 },
22 path::{Path, PathBuf},
23};
24
25use btoi::btoi;
26use memchr::{
27 arch::all::{is_equal, is_prefix, is_suffix, memchr::One},
28 memchr, memmem, memrchr,
29};
30use nix::{
31 errno::Errno,
32 fcntl::{openat2, OFlag, OpenHow, ResolveFlag, AT_FDCWD},
33 libc::pid_t,
34 unistd::Pid,
35 NixPath,
36};
37use once_cell::sync::Lazy;
38
39use crate::{
40 config::MAGIC_PREFIX,
41 fs::{retry_on_eintr, tgkill, FileType},
42 log::log_untrusted_buf,
43};
44
45#[macro_export]
47macro_rules! xpath {
48 ($($arg:tt)*) => {
49 XPathBuf::from(format!($($arg)*))
50 };
51}
52
53pub const PATH_MAX: usize = 4096;
55
56pub const PATH_MIN: usize = 64;
58
59static DOTDOT: Lazy<u64> = Lazy::new(|| c"..".as_ptr() as *const libc::c_char as u64);
61
62#[inline(always)]
63pub(crate) fn dotdot_with_nul() -> u64 {
64 *DOTDOT
65}
66
67#[allow(clippy::derived_hash_with_manual_eq)]
70#[derive(Clone, Hash, Ord, PartialOrd)]
71pub struct XPathBuf(Vec<u8>);
72
73impl Default for XPathBuf {
74 fn default() -> Self {
75 Self(Vec::with_capacity(PATH_MIN))
76 }
77}
78
79impl Eq for XPathBuf {}
80
81impl PartialEq for XPathBuf {
82 fn eq(&self, other: &Self) -> bool {
83 is_equal(&self.0, &other.0)
84 }
85}
86
87impl PartialEq<XPath> for XPathBuf {
88 fn eq(&self, other: &XPath) -> bool {
89 is_equal(self.as_bytes(), other.as_bytes())
90 }
91}
92
93impl PartialEq<XPathBuf> for XPath {
94 fn eq(&self, other: &XPathBuf) -> bool {
95 is_equal(self.as_bytes(), other.as_bytes())
96 }
97}
98
99impl Deref for XPathBuf {
100 type Target = XPath;
101
102 #[inline]
103 fn deref(&self) -> &XPath {
104 XPath::from_bytes(&self.0)
105 }
106}
107
108impl Borrow<XPath> for XPathBuf {
109 #[inline]
110 fn borrow(&self) -> &XPath {
111 self.deref()
112 }
113}
114
115#[allow(clippy::derived_hash_with_manual_eq)]
118#[repr(transparent)]
119#[derive(Hash, Ord, PartialOrd)]
120pub struct XPath(OsStr);
121
122impl Eq for XPath {}
123
124impl PartialEq for XPath {
125 fn eq(&self, other: &Self) -> bool {
126 is_equal(self.0.as_bytes(), other.0.as_bytes())
127 }
128}
129
130impl ToOwned for XPath {
131 type Owned = XPathBuf;
132
133 fn to_owned(&self) -> Self::Owned {
134 XPathBuf::from(self.as_bytes())
135 }
136}
137
138impl AsRef<XPath> for XPathBuf {
139 fn as_ref(&self) -> &XPath {
140 self.as_xpath()
141 }
142}
143
144impl AsRef<Path> for XPathBuf {
145 fn as_ref(&self) -> &Path {
146 self.as_path()
147 }
148}
149
150impl AsRef<OsStr> for XPathBuf {
151 fn as_ref(&self) -> &OsStr {
152 self.as_os_str()
153 }
154}
155
156impl From<&XPath> for XPathBuf {
157 fn from(path: &XPath) -> Self {
158 path.as_bytes().into()
159 }
160}
161
162impl From<PathBuf> for XPathBuf {
163 fn from(pbuf: PathBuf) -> Self {
164 pbuf.into_os_string().into()
165 }
166}
167
168impl From<&OsStr> for XPathBuf {
169 fn from(ostr: &OsStr) -> Self {
170 ostr.as_bytes().into()
171 }
172}
173
174impl From<OsString> for XPathBuf {
175 fn from(os: OsString) -> Self {
176 Self(os.into_vec())
177 }
178}
179
180impl From<String> for XPathBuf {
181 fn from(s: String) -> Self {
182 Self(s.as_bytes().into())
183 }
184}
185
186impl From<&str> for XPathBuf {
187 fn from(s: &str) -> Self {
188 Self(s.as_bytes().into())
189 }
190}
191
192impl From<Cow<'_, str>> for XPathBuf {
193 fn from(cow: Cow<'_, str>) -> Self {
194 match cow {
195 Cow::Borrowed(s) => Self(s.as_bytes().to_vec()),
196 Cow::Owned(s) => Self(s.into_bytes()),
197 }
198 }
199}
200
201impl From<&[u8]> for XPathBuf {
202 fn from(bytes: &[u8]) -> Self {
203 Self(bytes.to_vec())
204 }
205}
206
207impl From<Vec<u8>> for XPathBuf {
208 fn from(vec: Vec<u8>) -> Self {
209 Self(vec)
210 }
211}
212
213impl From<VecDeque<u8>> for XPathBuf {
214 fn from(vec: VecDeque<u8>) -> Self {
215 Self(vec.into())
216 }
217}
218
219impl From<pid_t> for XPathBuf {
220 fn from(pid: pid_t) -> Self {
221 let mut buf = itoa::Buffer::new();
222 buf.format(pid).into()
223 }
224}
225
226impl std::ops::Deref for XPath {
227 type Target = Path;
228
229 fn deref(&self) -> &Self::Target {
230 self.as_path()
231 }
232}
233
234impl AsRef<Path> for XPath {
235 fn as_ref(&self) -> &Path {
236 self.as_path()
237 }
238}
239
240impl AsRef<OsStr> for XPath {
241 fn as_ref(&self) -> &OsStr {
242 self.as_os_str()
243 }
244}
245
246impl std::fmt::Display for XPathBuf {
247 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
248 write!(f, "{}", mask_path(self.as_path()))
250 }
251}
252
253impl std::fmt::Debug for XPathBuf {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 write!(f, "{}", mask_path(self.as_path()))
257 }
258}
259
260impl serde::Serialize for XPathBuf {
261 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
262 where
263 S: serde::Serializer,
264 {
265 serializer.serialize_str(&format!("{self}"))
267 }
268}
269
270impl NixPath for XPathBuf {
271 fn is_empty(&self) -> bool {
272 self.0.is_empty()
273 }
274
275 fn len(&self) -> usize {
276 self.0.len()
277 }
278
279 fn with_nix_path<T, F>(&self, f: F) -> Result<T, Errno>
280 where
281 F: FnOnce(&CStr) -> T,
282 {
283 self.as_os_str().with_nix_path(f)
284 }
285}
286
287impl std::fmt::Display for XPath {
288 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
289 write!(f, "{}", mask_path(self.as_path()))
291 }
292}
293
294impl std::fmt::Debug for XPath {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 write!(f, "{}", mask_path(self.as_path()))
298 }
299}
300
301impl serde::Serialize for XPath {
302 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
303 where
304 S: serde::Serializer,
305 {
306 serializer.serialize_str(&format!("{self}"))
308 }
309}
310
311impl NixPath for XPath {
312 fn is_empty(&self) -> bool {
313 self.0.is_empty()
314 }
315
316 fn len(&self) -> usize {
317 self.0.len()
318 }
319
320 fn with_nix_path<T, F>(&self, f: F) -> Result<T, Errno>
321 where
322 F: FnOnce(&CStr) -> T,
323 {
324 self.as_os_str().with_nix_path(f)
325 }
326}
327
328impl XPath {
329 #[inline(always)]
344 pub fn check(
345 &self,
346 pid: Pid,
347 file_type: Option<&FileType>,
348 dir_entry: Option<&XPath>,
349 safe_name: bool,
350 ) -> Result<(), Errno> {
351 if matches!(file_type, Some(FileType::Blk | FileType::Unk)) {
356 return Err(Errno::ENOENT);
357 }
358 let is_mfd = matches!(file_type, Some(FileType::Mfd));
368 let is_proc_dir = self.starts_with(b"/proc");
369 if safe_name && !is_mfd && !is_proc_dir && self.check_name().is_err() {
370 return Err(Errno::EINVAL);
371 }
372 let (is_proc, proc_pid) = if is_proc_dir {
376 const LEN: usize = b"/proc".len();
377 let mut proc_pid = None;
378 let is_proc = self.len() == LEN;
379
380 if is_proc {
381 if let Some(p) = dir_entry {
383 proc_pid = btoi::<libc::pid_t>(p.as_bytes()).ok();
384 }
385 }
386
387 if proc_pid.is_none()
388 && self
389 .get(LEN + 1)
390 .map(|c| c.is_ascii_digit())
391 .unwrap_or(false)
392 {
393 let path = self.as_bytes();
394 let path = &path[LEN + 1..];
395 let pidx = memchr(b'/', path).unwrap_or(path.len());
396 proc_pid = btoi::<libc::pid_t>(&path[..pidx]).ok();
397 }
398
399 (is_proc, proc_pid)
400 } else {
401 return Ok(());
402 };
403 let proc_pid = if let Some(pid) = proc_pid {
404 pid
405 } else {
406 return Ok(());
407 };
408
409 if is_proc && proc_pid != pid.as_raw() {
414 return Err(Errno::ENOENT);
415 }
416 let syd_pid = Pid::this();
425 let proc_pid = Pid::from_raw(proc_pid);
426 if proc_pid == syd_pid {
427 return Err(Errno::ENOENT);
428 }
429 if tgkill(syd_pid, proc_pid, 0).is_ok() {
432 return Err(Errno::ENOENT);
433 }
434 Ok(())
438 }
439
440 #[allow(clippy::arithmetic_side_effects)]
484 pub fn check_name(&self) -> Result<(), Errno> {
485 let (_, name) = self.split();
486 let name = name.as_bytes();
487 let len = name.len();
488
489 if len == 0 {
490 return Err(Errno::EINVAL);
491 }
492
493 let name_utf8 = std::str::from_utf8(name).or(Err(Errno::EINVAL))?;
495
496 if name_utf8
499 .chars()
500 .nth(0)
501 .map(|c| c.is_whitespace())
502 .unwrap_or(false)
503 {
504 return Err(Errno::EINVAL);
505 }
506 if name_utf8
507 .chars()
508 .last()
509 .map(|c| c.is_whitespace())
510 .unwrap_or(false)
511 {
512 return Err(Errno::EINVAL);
513 }
514
515 let first_byte = name[0];
516 let last_byte = name[len - 1];
517
518 if !is_permitted_initial(first_byte) {
520 return Err(Errno::EINVAL);
521 }
522
523 match len {
525 2 => {
526 let middle_byte = name[1];
528 if !is_permitted_middle(middle_byte) {
529 return Err(Errno::EINVAL);
530 }
531 }
532 n if n > 2 => {
533 for &b in &name[1..len - 1] {
534 if !is_permitted_middle(b) {
535 return Err(Errno::EINVAL);
536 }
537 }
538 }
539 _ => {}
540 }
541
542 if !is_permitted_final(last_byte) {
544 return Err(Errno::EINVAL);
545 }
546
547 Ok(())
548 }
549
550 #[allow(clippy::arithmetic_side_effects)]
554 pub fn split_prefix(&self, base: &[u8]) -> Option<&Self> {
555 let mut len = base.len();
556 if len == 0 {
557 return None;
558 } else if base == b"/" {
559 return Some(self);
560 }
561
562 let base = if base[len - 1] == b'/' {
563 len -= 1;
564 &base[..len - 1]
565 } else {
566 base
567 };
568
569 if !self.starts_with(base) {
570 return None;
571 }
572
573 let raw = self.as_bytes();
574 let len_raw = raw.len();
575 if len == len_raw {
576 Some(XPath::from_bytes(b""))
577 } else if len_raw < len + 1 || raw[len] != b'/' {
578 None
579 } else {
580 Some(XPath::from_bytes(&raw[len + 1..]))
581 }
582 }
583
584 #[allow(clippy::arithmetic_side_effects)]
590 pub fn split(&self) -> (&Self, &Self) {
591 let bytes = match self.get(0) {
593 None => return (XPath::from_bytes(b""), XPath::from_bytes(b"")),
594 Some(b'/') if self.0.len() == 1 => {
595 return (
596 XPath::from_bytes(&self.as_bytes()[..1]),
597 XPath::from_bytes(&self.as_bytes()[..1]),
598 )
599 }
600 _ => self.as_bytes(),
601 };
602
603 let has_trailing_slash = bytes[bytes.len() - 1] == b'/';
605 let effective_length = if has_trailing_slash && bytes.len() > 1 {
606 bytes.len() - 1
607 } else {
608 bytes.len()
609 };
610 let last_slash_index = memrchr(b'/', &bytes[..effective_length]);
611
612 if let Some(idx) = last_slash_index {
613 let parent_path = if idx == 0 {
614 XPath::from_bytes(b"/")
616 } else {
617 XPath::from_bytes(&bytes[..idx])
619 };
620
621 let filename_start = idx + 1;
622 let filename_end = if has_trailing_slash {
623 bytes.len()
624 } else {
625 effective_length
626 };
627 let filename_path = XPath::from_bytes(&bytes[filename_start..filename_end]);
628
629 return (parent_path, filename_path);
630 }
631
632 (XPath::from_bytes(b""), self)
634 }
635
636 pub fn extension(&self) -> Option<&Self> {
638 let dot = memrchr(b'.', self.as_bytes())?;
639 #[allow(clippy::arithmetic_side_effects)]
641 if dot < self.0.len() - 1 {
642 Some(Self::from_bytes(&self.as_bytes()[dot + 1..]))
643 } else {
644 None
645 }
646 }
647
648 pub fn parent(&self) -> &Self {
650 Self::from_bytes(&self.as_bytes()[..self.parent_len()])
651 }
652
653 #[allow(clippy::arithmetic_side_effects)]
655 pub fn parent_len(&self) -> usize {
656 let bytes = match self.get(0) {
658 None => return 0,
659 Some(b'/') if self.len() == 1 => return 1,
660 _ => self.as_bytes(),
661 };
662
663 let has_trailing_slash = bytes[bytes.len() - 1] == b'/';
665 let effective_length = if has_trailing_slash && bytes.len() > 1 {
666 bytes.len() - 1
667 } else {
668 bytes.len()
669 };
670 let last_slash_index = memrchr(b'/', &bytes[..effective_length]);
671
672 if let Some(idx) = last_slash_index {
673 return if idx == 0 {
674 1
676 } else {
677 idx
679 };
680 }
681
682 0
684 }
685
686 pub fn depth(&self) -> usize {
690 memchr::arch::all::memchr::One::new(b'/').count(self.as_bytes())
691 }
692
693 pub fn descendant_of(&self, root: &[u8]) -> bool {
696 if is_equal(root, b"/") {
697 return true;
699 } else if !self.starts_with(root) {
700 return false;
702 }
703
704 let slen = self.len();
705 let rlen = root.len();
706
707 match slen.cmp(&rlen) {
708 Ordering::Less => false,
709 Ordering::Equal => true,
710 Ordering::Greater => self.get(rlen) == Some(b'/'),
711 }
712 }
713
714 pub fn strip_prefix(&self, base: &[u8]) -> Option<&Self> {
725 if !self.starts_with(base) {
726 return None;
727 }
728
729 let remainder = &self.as_bytes()[base.len()..];
731
732 if remainder.is_empty() {
734 Some(Self::from_bytes(b""))
736 } else if remainder[0] == b'/' {
737 Some(Self::from_bytes(&remainder[1..]))
740 } else {
741 None
743 }
744 }
745
746 #[allow(clippy::arithmetic_side_effects)]
752 #[allow(clippy::if_same_then_else)]
753 pub fn ends_with_dot(&self) -> bool {
754 let bytes = self.as_bytes();
755
756 let mut index = bytes.len();
758 if index == 0 {
759 return false;
760 }
761
762 while index > 0 && bytes[index - 1] == b'/' {
764 index -= 1;
765 }
766
767 if index == 0 {
770 return false;
771 }
772
773 if bytes[index - 1] == b'.' {
775 if index == 1 || bytes[index - 2] == b'/' {
776 return true; } else if index > 1
778 && bytes[index - 2] == b'.'
779 && (index == 2 || bytes[index - 3] == b'/')
780 {
781 return true; }
783 }
784
785 false
786 }
787
788 pub fn ends_with_slash(&self) -> bool {
790 !self.is_rootfs() && self.last() == Some(b'/')
791 }
792
793 pub fn has_parent_dot(&self) -> bool {
795 self.contains(b"/..") || self.is_equal(b"..")
796 }
797
798 pub fn is_magic(&self) -> bool {
800 self.starts_with(MAGIC_PREFIX)
801 }
802
803 pub fn is_rootfs(&self) -> bool {
805 self.as_bytes().iter().all(|b| matches!(*b, b'/' | b'.'))
806 }
807
808 pub fn is_procfs(&self) -> bool {
812 const PROC_LEN: usize = b"/proc".len();
813 const PROC_DIR_LEN: usize = b"/proc/".len();
814
815 match self.len() {
816 PROC_LEN if self.is_equal(b"/proc") => true,
817 PROC_DIR_LEN if self.is_equal(b"/proc/") => true,
818 _ => false,
819 }
820 }
821
822 pub fn is_dev(&self) -> bool {
827 self.starts_with(b"/dev/")
828 }
829
830 pub fn is_proc(&self) -> bool {
835 self.starts_with(b"/proc/")
836 }
837
838 pub fn is_static(&self) -> bool {
841 self.is_rootfs() || self.is_procfs() || self.is_equal(b"/dev/null")
842 }
843
844 pub fn is_proc_pid(&self) -> bool {
847 if !self.is_proc() {
848 return false;
849 }
850 match self.get("/proc/".len()) {
851 Some(n) => n.is_ascii_digit(),
852 None => false,
853 }
854 }
855
856 pub fn is_proc_self(&self, thread: bool) -> bool {
859 if thread {
860 is_equal(self.as_bytes(), b"/proc/thread-self")
861 } else {
862 is_equal(self.as_bytes(), b"/proc/self")
863 }
864 }
865
866 #[allow(clippy::disallowed_methods)]
868 pub fn exists(&self, follow: bool) -> bool {
869 let flags = if self.is_empty() {
870 return false;
871 } else if !follow {
872 OFlag::O_NOFOLLOW
873 } else {
874 OFlag::empty()
875 };
876
877 let mut how = OpenHow::new().flags(flags | OFlag::O_PATH | OFlag::O_CLOEXEC);
878 if !follow {
879 how =
880 how.resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
881 }
882
883 retry_on_eintr(|| openat2(AT_FDCWD, self, how))
884 .map(drop)
885 .is_ok()
886 }
887
888 pub fn is_symlink(&self) -> bool {
890 self.as_path().is_symlink()
891 }
892
893 pub fn is_dir(&self) -> bool {
895 self.as_path().is_dir()
896 }
897
898 pub fn is_file(&self) -> bool {
900 self.as_path().is_file()
901 }
902
903 pub fn is_absolute(&self) -> bool {
905 self.first() == Some(b'/')
906 }
907
908 pub fn is_relative(&self) -> bool {
912 !self.is_absolute()
913 }
914
915 pub fn is_dot(&self) -> bool {
917 let bytes = self.as_bytes();
918
919 if bytes.is_empty() {
920 return false;
921 }
922
923 let mut has_component = false;
924 let mut start = 0;
925
926 #[allow(clippy::arithmetic_side_effects)]
928 for sep in One::new(b'/').iter(bytes) {
929 if sep > start {
930 let component = &bytes[start..sep];
932 has_component = true;
933 if component != b"." {
934 return false;
935 }
936 }
937
938 start = sep + 1;
940 }
941
942 if start < bytes.len() {
945 let component = &bytes[start..];
946 has_component = true;
947 if component != b"." {
948 return false;
949 }
950 }
951
952 has_component
953 }
954
955 pub fn is_equal(&self, s: &[u8]) -> bool {
957 is_equal(self.as_bytes(), s)
958 }
959
960 pub fn starts_with(&self, base: &[u8]) -> bool {
962 is_prefix(self.as_bytes(), base)
963 }
964
965 pub fn ends_with(&self, base: &[u8]) -> bool {
967 is_suffix(self.as_bytes(), base)
968 }
969
970 pub fn contains(&self, sub: &[u8]) -> bool {
972 memmem::find_iter(self.as_bytes(), sub).next().is_some()
973 }
974
975 pub fn contains_char(&self, c: u8) -> bool {
977 memchr(c, self.as_bytes()).is_some()
978 }
979
980 pub fn first(&self) -> Option<u8> {
983 self.as_bytes().first().copied()
984 }
985
986 pub fn last(&self) -> Option<u8> {
989 self.as_bytes().last().copied()
990 }
991
992 pub fn get(&self, index: usize) -> Option<u8> {
995 self.as_bytes().get(index).copied()
996 }
997
998 pub fn as_path(&self) -> &Path {
1000 Path::new(self.as_os_str())
1001 }
1002
1003 pub fn join(&self, path: &[u8]) -> XPathBuf {
1006 let mut owned = self.to_owned();
1007 owned.push(path);
1008 owned
1009 }
1010
1011 pub fn as_bytes(&self) -> &[u8] {
1013 self.0.as_bytes()
1014 }
1015
1016 pub fn as_os_str(&self) -> &OsStr {
1018 &self.0
1019 }
1020
1021 pub const fn from_bytes(slice: &[u8]) -> &XPath {
1023 unsafe { std::mem::transmute(slice) }
1025 }
1026
1027 pub fn dotdot() -> &'static XPath {
1029 XPath::from_bytes(b"..")
1030 }
1031
1032 pub fn dot() -> &'static XPath {
1034 XPath::from_bytes(b".")
1035 }
1036
1037 pub fn root() -> &'static XPath {
1039 XPath::from_bytes(b"/")
1040 }
1041
1042 pub fn empty() -> &'static XPath {
1044 XPath::from_bytes(b"")
1045 }
1046
1047 pub fn new<S: AsRef<OsStr> + ?Sized>(s: &S) -> &XPath {
1049 unsafe { &*(s.as_ref() as *const OsStr as *const XPath) }
1051 }
1052}
1053
1054impl XPathBuf {
1055 pub fn clean_consecutive_slashes(&mut self) {
1060 let len = match self.len() {
1061 0 | 1 => return,
1062 n => n,
1063 };
1064
1065 let mut write_pos = 0;
1066 let mut read_pos = 0;
1067 #[allow(clippy::arithmetic_side_effects)]
1068 while read_pos < len {
1069 if self.0[read_pos] == b'/' {
1070 self.0[write_pos] = b'/';
1072 write_pos += 1;
1073 read_pos += 1;
1074
1075 while read_pos < len && self.0[read_pos] == b'/' {
1077 read_pos += 1;
1078 }
1079 } else {
1080 let next_slash = memchr(b'/', &self.0[read_pos..])
1082 .map(|pos| pos + read_pos)
1083 .unwrap_or(len);
1084
1085 let segment_len = next_slash - read_pos;
1086
1087 if read_pos != write_pos {
1089 self.0.copy_within(read_pos..next_slash, write_pos);
1090 }
1091
1092 write_pos += segment_len;
1093 read_pos = next_slash;
1094 }
1095 }
1096
1097 self.0.truncate(write_pos);
1099 }
1100
1101 pub fn from_pid(pid: Pid) -> Self {
1103 let mut buf = itoa::Buffer::new();
1104 buf.format(pid.as_raw()).as_bytes().into()
1105 }
1106
1107 pub fn from_fd(fd: RawFd) -> Self {
1109 let mut buf = itoa::Buffer::new();
1110 buf.format(fd).as_bytes().into()
1111 }
1112
1113 pub fn from_self_fd(fd: RawFd) -> Self {
1117 let mut pfd = Self::from("thread-self/fd");
1121 pfd.push_fd(fd);
1122 pfd
1123 }
1124
1125 pub fn push_pid(&mut self, pid: Pid) {
1127 let mut buf = itoa::Buffer::new();
1128 self.push(buf.format(pid.as_raw()).as_bytes())
1129 }
1130
1131 pub fn push_fd(&mut self, fd: RawFd) {
1133 let mut buf = itoa::Buffer::new();
1134 self.push(buf.format(fd).as_bytes())
1135 }
1136
1137 pub fn push(&mut self, path: &[u8]) {
1139 if path.first() == Some(&b'/') {
1140 self.0.clear();
1142 } else if self.last().map(|c| c != b'/').unwrap_or(true) {
1143 self.append_byte(b'/');
1145 }
1146 self.append_bytes(path);
1148 }
1149
1150 pub fn pop(&mut self) {
1152 self.truncate(self.parent_len());
1153 }
1154
1155 #[inline]
1162 pub unsafe fn pop_unchecked(&mut self) {
1163 #[allow(clippy::arithmetic_side_effects)]
1164 if let Some(idx) = memrchr(b'/', &self.as_bytes()[1..]) {
1165 self.0.truncate(idx + 1);
1166 } else if self.0.len() > 1 {
1167 self.0.truncate(1);
1168 }
1169 }
1170
1171 pub fn append_bytes(&mut self, bytes: &[u8]) {
1173 self.0.extend(bytes.iter().copied())
1174 }
1175
1176 pub fn append_byte(&mut self, byte: u8) {
1178 self.0.push(byte)
1179 }
1180
1181 pub fn pop_last(&mut self) -> Option<u8> {
1183 self.0.pop()
1184 }
1185
1186 pub fn into_vec(self) -> Vec<u8> {
1188 self.0
1189 }
1190
1191 pub fn into_os_string(self) -> OsString {
1193 OsString::from_vec(self.0)
1194 }
1195
1196 pub fn truncate(&mut self, len: usize) {
1200 self.0.truncate(len)
1201 }
1202
1203 pub fn remove(&mut self, index: usize) -> u8 {
1206 self.0.remove(index)
1207 }
1208
1209 pub fn shrink_to_fit(&mut self) {
1214 self.0.shrink_to_fit()
1215 }
1216
1217 pub fn try_reserve(&mut self, additional: usize) -> Result<(), Errno> {
1220 self.0.try_reserve(additional).or(Err(Errno::ENOMEM))
1221 }
1222
1223 pub fn empty() -> Self {
1225 Self(vec![])
1226 }
1227
1228 pub fn with_capacity(n: usize) -> Self {
1230 Self(Vec::with_capacity(n))
1231 }
1232
1233 pub fn capacity(&self) -> usize {
1235 self.0.capacity()
1236 }
1237
1238 pub fn join(&self, path: &[u8]) -> XPathBuf {
1241 let mut owned = self.clone();
1242 owned.push(path);
1243 owned
1244 }
1245
1246 pub fn as_bytes(&self) -> &[u8] {
1248 &self.0
1249 }
1250
1251 pub fn as_os_str(&self) -> &OsStr {
1253 OsStr::from_bytes(&self.0)
1254 }
1255
1256 pub fn as_path(&self) -> &Path {
1258 Path::new(self.as_os_str())
1259 }
1260
1261 pub fn as_xpath(&self) -> &XPath {
1263 XPath::new(self.as_os_str())
1264 }
1265
1266 pub fn is_symlink(&self) -> bool {
1268 self.as_path().is_symlink()
1269 }
1270
1271 pub fn is_dir(&self) -> bool {
1273 self.as_path().is_dir()
1274 }
1275
1276 pub fn is_file(&self) -> bool {
1278 self.as_path().is_file()
1279 }
1280
1281 #[inline]
1283 pub fn as_slice(&self) -> &[u8] {
1284 self.0.as_slice()
1285 }
1286
1287 #[inline]
1289 pub fn as_mut_slice(&mut self) -> &mut [u8] {
1290 self.0.as_mut_slice()
1291 }
1292
1293 #[inline]
1295 pub fn as_ptr(&self) -> *const u8 {
1296 self.0.as_ptr()
1297 }
1298
1299 #[inline]
1301 pub fn as_mut_ptr(&mut self) -> *mut u8 {
1302 self.0.as_mut_ptr()
1303 }
1304
1305 #[inline]
1312 pub unsafe fn set_len(&mut self, new_len: usize) {
1313 self.0.set_len(new_len)
1314 }
1315}
1316
1317#[inline]
1320pub fn mask_path(path: &Path) -> String {
1321 let (mask, _) = log_untrusted_buf(path.as_os_str().as_bytes());
1322 mask
1323}
1324
1325#[inline]
1326fn is_permitted_initial(b: u8) -> bool {
1327 is_permitted_byte(b) && !matches!(b, b'-' | b' ' | b'~')
1328}
1329
1330#[inline]
1331fn is_permitted_middle(b: u8) -> bool {
1332 is_permitted_byte(b)
1333}
1334
1335#[inline]
1336fn is_permitted_final(b: u8) -> bool {
1337 is_permitted_byte(b) && b != b' '
1338}
1339
1340#[inline]
1341fn is_permitted_byte(b: u8) -> bool {
1342 match b {
1343 b'*' | b'?' | b':' | b'[' | b']' | b'"' | b'<' | b'>' | b'|' | b'(' | b')' | b'{'
1344 | b'}' | b'&' | b'\'' | b'!' | b'\\' | b';' | b'$' | b'`' => false,
1345 0x20..=0x7E => true,
1346 0x80..=0xFE => true,
1347 _ => false,
1348 }
1349}
1350
1351#[cfg(test)]
1352mod tests {
1353 use std::{sync::mpsc, thread};
1354
1355 use nix::unistd::{gettid, pause};
1356
1357 use super::*;
1358
1359 struct CCSTestCase<'a> {
1360 src: &'a str,
1361 dst: &'a str,
1362 }
1363
1364 const CCS_TESTS: &[CCSTestCase] = &[
1365 CCSTestCase { src: "/", dst: "/" },
1366 CCSTestCase {
1367 src: "///",
1368 dst: "/",
1369 },
1370 CCSTestCase {
1371 src: "////",
1372 dst: "/",
1373 },
1374 CCSTestCase {
1375 src: "//home/alip///",
1376 dst: "/home/alip/",
1377 },
1378 CCSTestCase {
1379 src: "//home/alip///.config///",
1380 dst: "/home/alip/.config/",
1381 },
1382 CCSTestCase {
1383 src: "//home/alip///.config///htop////",
1384 dst: "/home/alip/.config/htop/",
1385 },
1386 CCSTestCase {
1387 src: "//home/alip///.config///htop////htoprc",
1388 dst: "/home/alip/.config/htop/htoprc",
1389 },
1390 ];
1391
1392 #[test]
1393 fn test_clean_consecutive_slashes() {
1394 for (idx, test) in CCS_TESTS.iter().enumerate() {
1395 let mut path = XPathBuf::from(test.src);
1396 path.clean_consecutive_slashes();
1397 assert_eq!(
1398 path,
1399 XPathBuf::from(test.dst),
1400 "Test {idx}: {} -> {path} != {}",
1401 test.src,
1402 test.dst
1403 );
1404 }
1405 }
1406
1407 struct EndsWithDotTestCase<'a> {
1408 path: &'a str,
1409 test: bool,
1410 }
1411
1412 const ENDS_WITH_DOT_TESTS: &[EndsWithDotTestCase] = &[
1413 EndsWithDotTestCase {
1414 path: ".",
1415 test: true,
1416 },
1417 EndsWithDotTestCase {
1418 path: "..",
1419 test: true,
1420 },
1421 EndsWithDotTestCase {
1422 path: "...",
1423 test: false,
1424 },
1425 EndsWithDotTestCase {
1426 path: "/.",
1427 test: true,
1428 },
1429 EndsWithDotTestCase {
1430 path: "/..",
1431 test: true,
1432 },
1433 EndsWithDotTestCase {
1434 path: "/...",
1435 test: false,
1436 },
1437 EndsWithDotTestCase {
1438 path: "foo.",
1439 test: false,
1440 },
1441 EndsWithDotTestCase {
1442 path: "foo./.",
1443 test: true,
1444 },
1445 EndsWithDotTestCase {
1446 path: "foo/./././/./",
1447 test: true,
1448 },
1449 EndsWithDotTestCase {
1450 path: "conftest.dir/././././////",
1451 test: true,
1452 },
1453 ];
1454
1455 #[test]
1456 fn test_ends_with_dot() {
1457 for (idx, test) in ENDS_WITH_DOT_TESTS.iter().enumerate() {
1458 let ends = XPath::from_bytes(test.path.as_bytes()).ends_with_dot();
1459 assert_eq!(
1460 test.test, ends,
1461 "EndsWithDotTestCase {} -> \"{}\": {} != {}",
1462 idx, test.path, test.test, ends
1463 );
1464 }
1465 }
1466
1467 #[test]
1468 fn test_is_dot() {
1469 let cases = [
1470 (".", true),
1471 ("./", true),
1472 (".///", true),
1473 ("././", true),
1474 ("./././", true),
1475 ("././././", true),
1476 ("/././", true),
1477 ("/./././", true),
1478 (".//././", true),
1479 ("", false),
1480 ("/", false),
1481 ("..", false),
1482 ("./..", false),
1483 ("../", false),
1484 ("././..", false),
1485 ("./../", false),
1486 ("./a", false),
1487 ("a/.", false),
1488 ("././a", false),
1489 ("a/./.", false),
1490 ("./././..", false),
1491 ("./.hidden", false),
1492 ("././.hidden", false),
1493 ("some/./path", false),
1494 ("./some/path", false),
1495 ("some/path/.", false),
1496 ("/some/path", false),
1497 ];
1498
1499 for &(input, expected) in &cases {
1500 let path = XPath::from_bytes(input.as_bytes());
1501 assert_eq!(path.is_dot(), expected, "Failed on input: {:?}", input);
1502 }
1503 }
1504
1505 #[test]
1506 fn test_descendant_of() {
1507 let cases = [
1508 ("/", "/", true),
1509 ("/foo", "/", true),
1510 ("/foo/bar", "/", true),
1511 ("/foo", "/foo", true),
1512 ("/foo/bar", "/foo", true),
1513 ("/foo2", "/foo", false),
1514 ("/foot", "/foo", false),
1515 ("/fo", "/foo", false),
1516 ("/", "/foo", false),
1517 ("/foo/bar", "/foo/bar", true),
1518 ("/foo/bar/baz", "/foo/bar", true),
1519 ("/foo/barbaz", "/foo/bar", false),
1520 ("/foo", "/foo/bar", false),
1521 ];
1522
1523 for &(path, root, expected) in &cases {
1524 let path = XPath::from_bytes(path.as_bytes());
1525 assert_eq!(
1526 path.descendant_of(root.as_bytes()),
1527 expected,
1528 "Failed on input: {path:?} of {root}!"
1529 );
1530 }
1531 }
1532
1533 #[test]
1534 fn test_path_check_file_type() {
1535 assert!(XPathBuf::from("/proc")
1536 .check(Pid::from_raw(1), Some(&FileType::Dir), None, true)
1537 .is_ok());
1538 assert!(XPathBuf::from("/proc")
1539 .check(
1540 Pid::from_raw(1),
1541 Some(&FileType::Dir),
1542 Some(&XPath::from_bytes(b"self")),
1543 true,
1544 )
1545 .is_ok());
1546 assert!(XPathBuf::from("/proc")
1547 .check(
1548 Pid::from_raw(1),
1549 Some(&FileType::Reg),
1550 Some(&XPath::from_bytes(b"uptime")),
1551 true,
1552 )
1553 .is_ok());
1554 assert!(XPathBuf::from("/dev/null")
1555 .check(Pid::from_raw(1), Some(&FileType::Chr), None, true)
1556 .is_ok());
1557 assert!(XPathBuf::from("/dev/log")
1558 .check(Pid::from_raw(1), Some(&FileType::Sock), None, true)
1559 .is_ok());
1560 assert!(XPathBuf::from("/dev/fifo")
1561 .check(Pid::from_raw(1), Some(&FileType::Fifo), None, true)
1562 .is_ok());
1563 assert!(XPathBuf::from("/dev/sda1")
1564 .check(Pid::from_raw(1), Some(&FileType::Blk), None, true)
1565 .is_err());
1566 assert!(XPathBuf::from("/dev/lmao")
1567 .check(Pid::from_raw(1), Some(&FileType::Unk), None, true)
1568 .is_err());
1569 }
1570
1571 #[test]
1572 fn test_path_check_procfs() {
1573 let this = Pid::from_raw(128);
1574 let that = Pid::from_raw(256);
1575 assert!(XPathBuf::from("/proc")
1576 .check(this, Some(&FileType::Dir), Some(&xpath!("{this}")), true,)
1577 .is_ok());
1578 assert!(XPathBuf::from(format!("/proc/{this}"))
1579 .check(
1580 this,
1581 Some(&FileType::Reg),
1582 Some(&XPath::from_bytes(b"mem")),
1583 true,
1584 )
1585 .is_ok());
1586 assert!(XPathBuf::from(format!("/proc/{this}"))
1587 .check(
1588 this,
1589 Some(&FileType::Dir),
1590 Some(&XPath::from_bytes(b"")),
1591 true,
1592 )
1593 .is_ok());
1594 assert!(XPathBuf::from(format!("/proc/{this}/task"))
1595 .check(this, Some(&FileType::Dir), Some(&xpath!("{this}")), true,)
1596 .is_ok());
1597 assert!(XPathBuf::from("/proc")
1598 .check(this, Some(&FileType::Dir), Some(&xpath!("{that}")), true,)
1599 .is_err());
1600 assert!(XPathBuf::from(format!("/proc/{that}"))
1601 .check(
1602 this,
1603 Some(&FileType::Reg),
1604 Some(&XPath::from_bytes(b"")),
1605 true,
1606 )
1607 .is_ok());
1608 assert!(XPathBuf::from(format!("/proc/{that}"))
1609 .check(
1610 this,
1611 Some(&FileType::Dir),
1612 Some(&XPath::from_bytes(b"")),
1613 true,
1614 )
1615 .is_ok());
1616 assert!(XPathBuf::from(format!("/proc/{that}/task"))
1617 .check(this, Some(&FileType::Dir), Some(&xpath!("{that}")), true,)
1618 .is_ok());
1619 }
1620
1621 #[test]
1622 fn test_path_check_procfs_syd_leader() {
1623 let syd = Pid::this();
1624 assert!(XPathBuf::from("/proc")
1625 .check(syd, Some(&FileType::Dir), Some(&xpath!("{syd}")), true,)
1626 .is_err());
1627 assert!(XPathBuf::from(format!("/proc/{syd}"))
1628 .check(
1629 syd,
1630 Some(&FileType::Reg),
1631 Some(&XPath::from_bytes(b"")),
1632 true,
1633 )
1634 .is_err());
1635 assert!(XPathBuf::from(format!("/proc/{syd}"))
1636 .check(
1637 syd,
1638 Some(&FileType::Dir),
1639 Some(&XPath::from_bytes(b"")),
1640 true,
1641 )
1642 .is_err());
1643 assert!(XPathBuf::from(format!("/proc/{syd}/task"))
1644 .check(syd, Some(&FileType::Dir), Some(&xpath!("{syd}")), true,)
1645 .is_err());
1646 }
1647
1648 #[test]
1649 fn test_path_check_procfs_syd_thread() {
1650 let tid = {
1652 let (tx, rx) = mpsc::channel();
1653 thread::spawn(move || {
1654 tx.send(gettid()).unwrap();
1655 pause();
1656 });
1657 rx.recv().unwrap()
1658 };
1659 assert!(XPathBuf::from("/proc")
1660 .check(tid, Some(&FileType::Dir), Some(&xpath!("{tid}")), true,)
1661 .is_err());
1662 assert!(XPathBuf::from(format!("/proc/{tid}"))
1663 .check(
1664 tid,
1665 Some(&FileType::Reg),
1666 Some(&XPath::from_bytes(b"")),
1667 true,
1668 )
1669 .is_err());
1670 assert!(XPathBuf::from(format!("/proc/{tid}"))
1671 .check(
1672 tid,
1673 Some(&FileType::Dir),
1674 Some(&XPath::from_bytes(b"")),
1675 true,
1676 )
1677 .is_err());
1678 assert!(XPathBuf::from(format!("/proc/{tid}/task"))
1679 .check(tid, Some(&FileType::Dir), Some(&xpath!("{tid}")), true,)
1680 .is_err());
1681 }
1682
1683 #[test]
1684 fn test_path_split_prefix_absolute() {
1685 let path = XPathBuf::from("/tmp/foo/bar/baz");
1686
1687 assert_eq!(path.split_prefix(b"/").unwrap().as_bytes(), path.as_bytes());
1688
1689 assert!(path.split_prefix(b"/tm").is_none());
1690 assert_eq!(
1691 path.split_prefix(b"/tmp").unwrap().as_bytes(),
1692 b"foo/bar/baz"
1693 );
1694
1695 assert!(path.split_prefix(b"/tmp/f").is_none());
1696 assert_eq!(
1697 path.split_prefix(b"/tmp/foo/").unwrap().as_bytes(),
1698 b"bar/baz"
1699 );
1700
1701 assert_eq!(
1702 path.split_prefix(b"/tmp/foo/bar/baz").unwrap().as_bytes(),
1703 b""
1704 );
1705 }
1706
1707 #[test]
1708 fn test_path_split_prefix_relative() {
1709 let path = XPathBuf::from("tmp/foo/bar/baz");
1710
1711 assert!(path.split_prefix(b"t").is_none());
1712 assert!(path.split_prefix(b"tm").is_none());
1713
1714 assert_eq!(
1715 path.split_prefix(b"tmp").unwrap().as_bytes(),
1716 b"foo/bar/baz"
1717 );
1718 assert_eq!(
1719 path.split_prefix(b"tmp/").unwrap().as_bytes(),
1720 b"foo/bar/baz"
1721 );
1722
1723 assert_eq!(
1724 path.split_prefix(b"tmp/foo/bar/baz").unwrap().as_bytes(),
1725 b""
1726 );
1727 }
1728
1729 #[test]
1730 fn test_path_pop_unchecked() {
1731 let mut path = XPathBuf::from("/usr/host/bin/id");
1732 unsafe { path.pop_unchecked() };
1733 assert_eq!(path, XPathBuf::from("/usr/host/bin"));
1734 unsafe { path.pop_unchecked() };
1735 assert_eq!(path, XPathBuf::from("/usr/host"));
1736 unsafe { path.pop_unchecked() };
1737 assert_eq!(path, XPathBuf::from("/usr"));
1738 unsafe { path.pop_unchecked() };
1739 assert_eq!(path, XPathBuf::from("/"));
1740 unsafe { path.pop_unchecked() };
1741 assert_eq!(path, XPathBuf::from("/"));
1742 }
1743
1744 #[test]
1745 fn test_path_pop() {
1746 let mut path = XPathBuf::from("/spirited/away.rs");
1749 path.pop();
1750 assert_eq!(path, XPathBuf::from("/spirited"));
1751 path.pop();
1752 assert_eq!(path, XPathBuf::from("/"));
1753 path.pop();
1754 assert_eq!(path, XPathBuf::from("/"));
1755 }
1756
1757 #[test]
1758 fn test_path_push() {
1759 let mut path = XPathBuf::from("/tmp");
1761 path.push(b"file.bk");
1762 assert_eq!(path, XPathBuf::from("/tmp/file.bk"));
1763
1764 let mut path = XPathBuf::from("/tmp");
1766 path.push(b"/etc");
1767 assert_eq!(path, XPathBuf::from("/etc"));
1768
1769 let mut path = XPathBuf::from("/tmp/bar");
1770 path.push(b"baz/");
1771 assert_eq!(path, XPathBuf::from("/tmp/bar/baz/"));
1772
1773 let mut path = XPathBuf::from("/tmp");
1775 path.push(b"");
1776 assert_eq!(path, XPathBuf::from("/tmp/"));
1777 assert_eq!(path.as_os_str().as_bytes(), b"/tmp/");
1778 }
1779
1780 #[test]
1781 fn test_path_split() {
1782 let path = XPathBuf::from("/foo/bar/baz");
1784 let (parent, file_name) = path.split();
1785 assert_eq!(parent, XPath::from_bytes(b"/foo/bar"));
1786 assert_eq!(file_name, XPath::from_bytes(b"baz"));
1787
1788 let path = XPathBuf::from("/foo/bar/baz/");
1790 let (parent, file_name) = path.split();
1791 assert_eq!(parent, XPath::from_bytes(b"/foo/bar"));
1792 assert_eq!(file_name, XPath::from_bytes(b"baz/"));
1793
1794 let path = XPathBuf::from("/");
1796 let (parent, file_name) = path.split();
1797 assert_eq!(parent, XPath::from_bytes(b"/"));
1798 assert_eq!(file_name, XPath::from_bytes(b"/"));
1799
1800 let path = XPathBuf::from("/foo");
1802 let (parent, file_name) = path.split();
1803 assert_eq!(parent, XPath::from_bytes(b"/"));
1804 assert_eq!(file_name, XPath::from_bytes(b"foo"));
1805
1806 let path = XPathBuf::from("/foo/");
1808 let (parent, file_name) = path.split();
1809 assert_eq!(parent, XPath::from_bytes(b"/"));
1810 assert_eq!(file_name, XPath::from_bytes(b"foo/"));
1811 }
1812
1813 #[test]
1814 fn test_path_is_proc_pid() {
1815 assert!(XPathBuf::from("/proc/1").is_proc_pid());
1816 assert!(XPathBuf::from("/proc/1/").is_proc_pid());
1817
1818 assert!(XPathBuf::from("/proc/123456789").is_proc_pid());
1819 assert!(XPathBuf::from("/proc/123456789/task").is_proc_pid());
1820
1821 assert!(!XPathBuf::from("/proc").is_proc_pid());
1822 assert!(!XPathBuf::from("/proc/").is_proc_pid());
1823
1824 assert!(!XPathBuf::from("/proc/acpi").is_proc_pid());
1825 assert!(!XPathBuf::from("/proc/keys").is_proc_pid());
1826
1827 assert!(XPathBuf::from("/proc/0keys").is_proc_pid());
1829
1830 assert!(!XPathBuf::from("/dev").is_proc_pid());
1831 assert!(!XPathBuf::from("/dev/0").is_proc_pid());
1832
1833 assert!(!XPathBuf::from("/pro").is_proc_pid());
1834 assert!(!XPathBuf::from("/pro/").is_proc_pid());
1835 assert!(!XPathBuf::from("/pro/1").is_proc_pid());
1836 }
1837
1838 #[test]
1839 fn test_check_name_valid() {
1840 let valid_filenames = [
1841 "valid_filename.txt",
1842 "hello_world",
1843 "File123",
1844 "こんにちは", "文件", "emoji😀", "valid~name", "name~", "a",
1850 "normal",
1851 "test-file",
1852 "test_file",
1853 "file name",
1854 "file☃name", "name\u{0080}", "name\u{00FE}", "😀name", "name😀", "😀", "name😀name", "na~me", "name-", "name_", "name.", "a\u{0020}b", "a\u{00A0}b", "a\u{1680}b", "a\u{2007}b", "a\u{202F}b", "a\u{3000}b", ];
1872
1873 for (idx, name) in valid_filenames.iter().enumerate() {
1874 let name = XPath::new(name);
1875 assert!(
1876 name.check_name().is_ok(),
1877 "Filename {idx} '{name}' should be valid"
1878 );
1879 }
1880 }
1881
1882 #[test]
1883 fn test_check_name_invalid() {
1884 let invalid_filenames: &[&[u8]] = &[
1885 b"", b"-", b"*", b"?", b"!", b"$", b"`", b" -", b"~home", b"*home", b"?home", b"!home", b"$home", b"`home", b"file ", b"file*", b"file?", b"file!", b"file$", b"file`", b"bad*name", b"bad?name", b"bad!name", b"bad$name", b"bad`name", b"bad\nname", b"\0", b"bad\0name", b"bad\x7Fname", b"bad\xFFname", b"\x1Fcontrol", b"name\x1F", b"name\x7F", b"name\xFF", b"name ", b"-name", b" name", b"~name", b"*name", b"?name", b"!name", b"$name", b"`name", b"name\x19", b"name\n", b"\nname", b"na\nme", b"name\t", b"name\r", b"name\x1B", b"name\x00", b"name\x7F", b"name\xFF", b"\xFF", b"name\x80\xFF", b"name\xC0\xAF", b"\xF0\x28\x8C\xBC", b"\xF0\x90\x28\xBC", b"\xF0\x28\x8C\x28", b"name\xFFname", b"name\xC3\x28", b"name\xA0\xA1", b"\xE2\x28\xA1", b"\xE2\x82\x28", b"\xF0\x28\x8C\xBC", b"\xF0\x90\x28\xBC", b"\xF0\x28\x8C\x28", b"\xC2\xA0", b"\x20file", b"file\x20", b"\xC2\xA0file", b"file\xE3\x80\x80", b"\xE2\x80\xAFfile", b"\xE2\x81\x9Ffile\xE2\x81\x9F", ];
1960
1961 for (idx, name) in invalid_filenames.iter().enumerate() {
1962 let name = XPath::from_bytes(name);
1963 assert!(
1964 name.check_name().is_err(),
1965 "Filename {idx} '{name}' should not be valid"
1966 );
1967 }
1968 }
1969
1970 #[test]
1971 fn test_check_name_control_characters() {
1972 for b in 0x00..=0x1F {
1973 if let Some(c) = char::from_u32(b as u32) {
1974 let name = format!("name{c}char");
1975 let name = XPath::new(&name);
1976 assert!(
1977 name.check_name().is_err(),
1978 "Filename with control character '\\x{b:02X}' should be invalid",
1979 );
1980 }
1981 }
1982 }
1983
1984 #[test]
1985 fn test_check_name_extended_ascii_characters() {
1986 for b in 0x80..=0xFE {
1987 if b == 0xFF {
1988 continue; }
1990 let mut bytes = b"name".to_vec();
1991 bytes.push(b);
1992 bytes.extend_from_slice(b"char");
1993 let name = OsStr::from_bytes(&bytes);
1994 let name = XPath::new(name);
1995 let result = name.check_name();
1996 if std::str::from_utf8(&bytes).is_ok() {
1997 assert!(result.is_ok(), "Filename with byte 0x{b:X} should be valid",);
1998 } else {
1999 assert!(
2000 result.is_err(),
2001 "Filename with invalid UTF-8 byte 0x{b:X} should be invalid",
2002 );
2003 }
2004 }
2005 }
2006
2007 #[test]
2008 fn test_check_name_edge_cases() {
2009 let valid_single_chars = [
2011 "a", "b", "Z", "9", "_", ".", "😀", ];
2013
2014 for (idx, name) in valid_single_chars.iter().enumerate() {
2015 let name = XPath::new(name);
2016 assert!(
2017 name.check_name().is_ok(),
2018 "Single-character filename {idx} '{name}' should be valid",
2019 );
2020 }
2021
2022 let invalid_single_chars: &[&[u8]] = &[
2023 b"-", b" ", b"~", b"*", b"?", b"\n", b"\r", b"\x7F", b"\x1F", b"\xFF", b"\0", b"\xC2\xA0", b"\x20", b"\xC2\xA0", b"\xE1\x9A\x80", b"\xE2\x80\x87", b"\xE2\x80\xAF", b"\xE3\x80\x80", ];
2042
2043 for (idx, name) in invalid_single_chars.iter().enumerate() {
2044 let name = XPath::from_bytes(name);
2045 assert!(
2046 name.check_name().is_err(),
2047 "Single-character filename {idx} '{name}' should be invalid",
2048 );
2049 }
2050 }
2051}