A cli program to easily handle .gitignore files
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

563 lines
26 KiB

  1. /*! Validator functions suitable for use with `Clap` and `StructOpt` */
  2. // Copyright 2017-2019, Stephan Sokolow
  3. use std::ffi::OsString;
  4. use std::fs::File;
  5. use std::path::{Component, Path};
  6. /// Special filenames which cannot be used for real files under Win32
  7. ///
  8. /// (Unless your app uses the `\\?\` path prefix to bypass legacy Win32 API compatibility
  9. /// limitations)
  10. ///
  11. /// **NOTE:** These are still reserved if you append an extension to them.
  12. ///
  13. /// Source: [Boost Path Name Portability Guide
  14. /// ](https://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm)
  15. pub const RESERVED_DOS_FILENAMES: &[&str] = &["AUX", "CON", "NUL", "PRN", // Comments for rustfmt
  16. "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", // Serial Ports
  17. "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", // Parallel Ports
  18. "CLOCK$" ]; // https://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm
  19. // TODO: Add the rest of the disallowed names from
  20. // https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations
  21. /// Module to contain the unsafety of an `unsafe` call to `access()`
  22. #[cfg(unix)]
  23. mod access {
  24. /// TODO: Make this wrapper portable
  25. /// <https://doc.rust-lang.org/book/conditional-compilation.html>
  26. /// TODO: Consider making `wrapped_access` typesafe using the `bitflags`
  27. /// crate `clap` pulled in
  28. use libc::{access, c_int, W_OK};
  29. use std::ffi::CString;
  30. use std::os::unix::ffi::OsStrExt;
  31. use std::path::Path;
  32. /// Lower-level safety wrapper shared by all probably_* functions I define
  33. /// TODO: Unit test **HEAVILY** (Has unsafe block. Here be dragons!)
  34. fn wrapped_access(abs_path: &Path, mode: c_int) -> bool {
  35. // Debug-time check that we're using the API properly
  36. // (Debug-only because relying on it in a release build grants a false
  37. // sense of security and, besides, access() is only really safe to use
  38. // as a way to abort early for convenience on errors that would still
  39. // be safe anyway.)
  40. debug_assert!(abs_path.is_absolute());
  41. // Make a null-terminated copy of the path for libc
  42. match CString::new(abs_path.as_os_str().as_bytes()) {
  43. // If we succeed, call access(2), convert the result into bool, and return it
  44. Ok(cstr) => unsafe { access(cstr.as_ptr(), mode) == 0 },
  45. // If we fail, return false because it can't be an access()ible path
  46. Err(_) => false,
  47. }
  48. }
  49. /// API suitable for a lightweight "fail early" check for whether a target
  50. /// directory is writable without worry that a fancy filesystem may be
  51. /// configured to allow write but deny deletion for the resulting test file.
  52. /// (It's been seen in the wild)
  53. ///
  54. /// Uses a name which helps to drive home the security hazard in access()
  55. /// abuse and hide the mode flag behind an abstraction so the user can't
  56. /// mess up unsafe{} (eg. On my system, "/" erroneously returns success)
  57. pub fn probably_writable<P: AsRef<Path> + ?Sized>(path: &P) -> bool {
  58. wrapped_access(path.as_ref(), W_OK)
  59. }
  60. #[cfg(test)]
  61. mod tests {
  62. use std::ffi::OsStr;
  63. use std::os::unix::ffi::OsStrExt; // TODO: Find a better way to produce invalid UTF-8
  64. use super::probably_writable;
  65. #[test]
  66. fn probably_writable_basic_functionality() {
  67. assert!(probably_writable(OsStr::new("/tmp"))); // OK Folder
  68. assert!(probably_writable(OsStr::new("/dev/null"))); // OK File
  69. assert!(!probably_writable(OsStr::new("/etc/shadow"))); // Denied File
  70. assert!(!probably_writable(OsStr::new("/etc/ssl/private"))); // Denied Folder
  71. assert!(!probably_writable(OsStr::new("/nonexistant_test_path"))); // Missing Path
  72. assert!(!probably_writable(OsStr::new("/tmp\0with\0null"))); // Bad CString
  73. assert!(!probably_writable(OsStr::from_bytes(b"/not\xffutf8"))); // Bad UTF-8
  74. assert!(!probably_writable(OsStr::new("/"))); // Root
  75. // TODO: Relative path
  76. // TODO: Non-UTF8 path that actually does exist and is writable
  77. }
  78. }
  79. }
  80. /// Test that the given path **should** be writable
  81. ///
  82. /// **TODO:** Implement a Windows version of this.
  83. ///
  84. /// Given that every relevant Windows API I can find seems to be a complex mess compared to
  85. /// `access(2)`, I'll probably just want to settle for the compromise I rejected and just try
  86. /// writing and then deleting a test file.
  87. #[cfg(unix)]
  88. pub fn path_output_dir<P: AsRef<Path> + ?Sized>(value: &P) -> Result<(), OsString> {
  89. let path = value.as_ref();
  90. // Test that the path is a directory
  91. // (Check before, not after, as an extra safety guard on the unsafe block)
  92. if !path.is_dir() {
  93. return Err(format!("Not a directory: {}", path.display()).into());
  94. }
  95. // TODO: Think about how to code this more elegantly (try! perhaps?)
  96. if let Ok(abs_pathbuf) = path.canonicalize() {
  97. if let Some(abs_path) = abs_pathbuf.to_str() {
  98. if self::access::probably_writable(abs_path) {
  99. return Ok(());
  100. }
  101. }
  102. }
  103. Err(format!("Would be unable to write to destination directory: {}", path.display()).into())
  104. }
  105. /// The given path is a file that can be opened for reading
  106. ///
  107. /// ## Use For:
  108. /// * Input file paths
  109. ///
  110. /// ## Relevant Conventions:
  111. /// * Commands which read from `stdin` by default should use `-f` to specify the input path.
  112. /// [[1]](http://www.catb.org/esr/writings/taoup/html/ch10s05.html)
  113. /// * Commands which read from files by default should use positional arguments to specify input
  114. /// paths.
  115. /// * Allow an arbitrary number of input paths if feasible.
  116. /// * Interpret a value of `-` to mean "read from `stdin`" if feasible.
  117. /// [[2]](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html)
  118. ///
  119. /// **Note:** The following command-lines, which interleave files and `stdin`, are a good test of
  120. /// how the above conventions should interact:
  121. ///
  122. /// data_source | my_utility_a header.dat - footer.dat > output.dat
  123. /// data_source | my_utility_b -f header.dat -f - -f footer.dat > output.dat
  124. ///
  125. /// ## Cautions:
  126. /// * This will momentarily open the given path for reading to verify that it is readable.
  127. /// However, relying on this to remain true will introduce a race condition. This validator is
  128. /// intended only to allow your program to exit as quickly as possible in the case of obviously
  129. /// bad input.
  130. /// * As a more reliable validity check, you are advised to open a handle to the file in question
  131. /// as early in your program's operation as possible, use it for all your interactions with the
  132. /// file, and keep it open until you are finished. This will both verify its validity and
  133. /// minimize the window in which another process could render the path invalid.
  134. pub fn path_readable_file<P: AsRef<Path> + ?Sized>(value: &P)
  135. -> std::result::Result<(), OsString> {
  136. let path = value.as_ref();
  137. if path.is_dir() {
  138. return Err(format!("{}: Input path must be a file, not a directory",
  139. path.display()).into());
  140. }
  141. // TODO: Why does this not fail on Linux? I forget what reading a directory actually does.
  142. File::open(path).map(|_| ()).map_err(|e| format!("{}: {}", path.display(), e).into())
  143. }
  144. // TODO: Implement path_readable_dir and path_readable for --recurse use-cases
  145. /// The given path is valid on all major filesystems and OSes
  146. ///
  147. /// ## Use For:
  148. /// * Output file or directory paths
  149. ///
  150. /// ## Relevant Conventions:
  151. /// * Use `-o` to specify the output path.
  152. /// [[1]](http://www.catb.org/esr/writings/taoup/html/ch10s05.html)
  153. /// [[2]](http://tldp.org/LDP/abs/html/standard-options.html)
  154. /// * Interpret a value of `-` to mean "Write output to stdout".
  155. /// [[3]](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html)
  156. /// * Because `-o` does not inherently indicate whether it expects a file or a directory, consider
  157. /// also providing a GNU-style long version with a name like `--outfile` to allow scripts which
  158. /// depend on your tool to be more self-documenting.
  159. ///
  160. /// ## Cautions:
  161. /// * To ensure files can be copied/moved without issue, this validator may impose stricter
  162. /// restrictions on filenames than your filesystem. Do *not* use it for input paths.
  163. /// * Other considerations, such as paths containing symbolic links with longer target names, may
  164. /// still cause your system to reject paths which pass this check.
  165. /// * As a more reliable validity check, you are advised to open a handle to the file in question
  166. /// as early in your program's operation as possible and keep it open until you are finished.
  167. /// This will both verify its validity and minimize the window in which another process could
  168. /// render the path invalid.
  169. ///
  170. /// ## Design Considerations: [[4]](https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits)
  171. /// * Many popular Linux filesystems impose no total length limit.
  172. /// * This function imposes a 32,760-character limit for compatibility with flash drives formatted
  173. /// FAT32 or exFAT.
  174. /// * Some POSIX API functions, such as `getcwd()` and `realpath()` rely on the `PATH_MAX`
  175. /// constant, which typically specifies a length of 4096 bytes including terminal `NUL`, but
  176. /// this is not enforced by the filesystem itself.
  177. /// [[4]](https://insanecoding.blogspot.com/2007/11/pathmax-simply-isnt.html)
  178. ///
  179. /// Programs which rely on libc for this functionality but do not attempt to canonicalize paths
  180. /// will usually work if you change the working directory and use relative paths.
  181. /// * The following lengths were considered too limiting to be enforced by this function:
  182. /// * The UDF filesystem used on DVDs imposes a 1023-byte length limit on paths.
  183. /// * When not using the `\\?\` prefix to disable legacy compatibility, Windows paths are
  184. /// limited to 260 characters, which was arrived at as `A:\MAX_FILENAME_LENGTH<NULL>`.
  185. /// [[5]](https://stackoverflow.com/a/1880453/435253)
  186. /// * ISO 9660 without Joliet or Rock Ridge extensions does not permit periods in directory
  187. /// names, directory trees more than 8 levels deep, or filenames longer than 32 characters.
  188. /// [[6]](https://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm)
  189. ///
  190. /// **TODO:**
  191. /// * Write another function for enforcing the limits imposed by targeting optical media.
  192. pub fn path_valid_portable<P: AsRef<Path> + ?Sized>(value: &P) -> Result<(), OsString> {
  193. #![allow(clippy::match_same_arms, clippy::decimal_literal_representation)]
  194. let path = value.as_ref();
  195. if path.as_os_str().is_empty() {
  196. Err("Path is empty".into())
  197. } else if path.as_os_str().len() > 32760 {
  198. // Limit length to fit on VFAT/exFAT when using the `\\?\` prefix to disable legacy limits
  199. // Source: https://en.wikipedia.org/wiki/Comparison_of_file_systems
  200. Err(format!("Path is too long ({} chars): {:?}", path.as_os_str().len(), path).into())
  201. } else {
  202. for component in path.components() {
  203. if let Component::Normal(string) = component {
  204. filename_valid_portable(string)?
  205. }
  206. }
  207. Ok(())
  208. }
  209. }
  210. /// The string is a valid file/folder name on all major filesystems and OSes
  211. ///
  212. /// ## Use For:
  213. /// * Output file or directory names within a parent directory specified through other means.
  214. ///
  215. /// ## Relevant Conventions:
  216. /// * Most of the time, you want to let users specify a full path via [`path_valid_portable`
  217. /// ](fn.path_valid_portable.html)instead.
  218. ///
  219. /// ## Cautions:
  220. /// * To ensure files can be copied/moved without issue, this validator may impose stricter
  221. /// restrictions on filenames than your filesystem. Do *not* use it for input filenames.
  222. /// * This validator cannot guarantee that a given filename will be valid once other
  223. /// considerations such as overall path length limits are taken into account.
  224. /// * As a more reliable validity check, you are advised to open a handle to the file in question
  225. /// as early in your program's operation as possible, use it for all your interactions with the
  226. /// file, and keep it open until you are finished. This will both verify its validity and
  227. /// minimize the window in which another process could render the path invalid.
  228. ///
  229. /// ## Design Considerations: [[3]](https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits)
  230. /// * In the interest of not inconveniencing users in the most common case, this validator imposes
  231. /// a 255-character length limit.
  232. /// * The eCryptFS home directory encryption offered by Ubuntu Linux imposes a 143-character
  233. /// length limit when filename encryption is enabled.
  234. /// [[4]](https://bugs.launchpad.net/ecryptfs/+bug/344878)
  235. /// * the Joliet extensions for ISO 9660 are specified to support only 64-character filenames and
  236. /// tested to support either 103 or 110 characters depending whether you ask the mkisofs
  237. /// developers or Microsoft. [[5]](https://en.wikipedia.org/wiki/Joliet_(file_system))
  238. /// * The [POSIX Portable Filename Character Set
  239. /// ](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282)
  240. /// is too restrictive to be baked into a general-purpose validator.
  241. ///
  242. /// **TODO:** Consider converting this to a private function that just exists as a helper for the
  243. /// path validator in favour of more specialized validators for filename patterns, prefixes, and/or
  244. /// suffixes, to properly account for how "you can specify a name bu not a path" generally
  245. /// comes about.
  246. pub fn filename_valid_portable<P: AsRef<Path> + ?Sized>(value: &P) -> Result<(), OsString> {
  247. #![allow(clippy::match_same_arms, clippy::else_if_without_else)]
  248. let path = value.as_ref();
  249. // TODO: Should I refuse incorrect Unicode normalization since Finder doesn't like it or just
  250. // advise users to run a normalization pass?
  251. // Source: https://news.ycombinator.com/item?id=16993687
  252. // Check that the length is within range
  253. let os_str = path.as_os_str();
  254. if os_str.len() > 255 {
  255. return Err(format!("File/folder name is too long ({} chars): {:?}",
  256. path.as_os_str().len(), path).into());
  257. } else if os_str.is_empty() {
  258. return Err("Path component is empty".into());
  259. }
  260. // Check for invalid characters
  261. let lossy_str = os_str.to_string_lossy();
  262. let last_char = lossy_str.chars().last().expect("getting last character");
  263. if [' ', '.'].iter().any(|&x| x == last_char) {
  264. // The Windows shell and UI don't support component names ending in periods or spaces
  265. // Source: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
  266. return Err("Windows forbids path components ending with spaces/periods".into());
  267. } else if lossy_str.as_bytes().iter().any(|c| match c {
  268. // invalid on all APIs which don't use counted strings like inside the NT kernel
  269. b'\0' => true,
  270. // invalid under FAT*, VFAT, exFAT, and NTFS
  271. 0x0..=0x1f | 0x7f | b'"' | b'*' | b'<' | b'>' | b'?' | b'|' => true,
  272. // POSIX path separator (invalid on Unixy platforms like Linux and BSD)
  273. b'/' => true,
  274. // HFS/Carbon path separator (invalid in filenames on MacOS and Mac filesystems)
  275. // DOS/Win32 drive separator (invalid in filenames on Windows and Windows filesystems)
  276. b':' => true,
  277. // DOS/Windows path separator (invalid in filenames on Windows and Windows filesystems)
  278. b'\\' => true,
  279. // let everything else through
  280. _ => false,
  281. }) {
  282. #[allow(clippy::use_debug)]
  283. return Err(format!("Path component contains invalid characters: {:?}", path).into());
  284. }
  285. // Reserved DOS filenames that still can't be used on modern Windows for compatibility
  286. if let Some(file_stem) = path.file_stem() {
  287. let stem = file_stem.to_string_lossy().to_uppercase();
  288. if RESERVED_DOS_FILENAMES.iter().any(|&x| x == stem) {
  289. Err(format!("Filename is reserved on Windows: {:?}", file_stem).into())
  290. } else {
  291. Ok(())
  292. }
  293. } else {
  294. Ok(())
  295. }
  296. }
  297. #[cfg(test)]
  298. mod tests {
  299. use super::*;
  300. use std::ffi::OsStr;
  301. #[cfg(unix)]
  302. use std::os::unix::ffi::OsStrExt;
  303. #[cfg(windows)]
  304. use std::os::windows::ffi::OsStringExt;
  305. #[test]
  306. #[cfg(unix)]
  307. fn path_output_dir_basic_functionality() {
  308. assert!(path_output_dir(OsStr::new("/")).is_err()); // Root
  309. assert!(path_output_dir(OsStr::new("/tmp")).is_ok()); // OK Folder
  310. assert!(path_output_dir(OsStr::new("/dev/null")).is_err()); // OK File
  311. assert!(path_output_dir(OsStr::new("/etc/shadow")).is_err()); // Denied File
  312. assert!(path_output_dir(OsStr::new("/etc/ssl/private")).is_err()); // Denied Folder
  313. assert!(path_output_dir(OsStr::new("/nonexistant_test_path")).is_err()); // Missing Path
  314. assert!(path_output_dir(OsStr::new("/tmp\0with\0null")).is_err()); // Invalid CString
  315. // TODO: is_dir but fails to canonicalize()
  316. // TODO: Not-already-canonicalized paths
  317. assert!(path_output_dir(OsStr::from_bytes(b"/not\xffutf8")).is_err()); // Invalid UTF-8
  318. // TODO: Non-UTF8 path that actually does exist and is writable
  319. }
  320. #[test]
  321. #[cfg(windows)]
  322. fn path_output_dir_basic_functionality() {
  323. unimplemented!("TODO: Implement Windows version of path_output_dir");
  324. }
  325. // ---- path_readable_file ----
  326. #[cfg(unix)]
  327. #[test]
  328. fn path_readable_file_basic_functionality() {
  329. // Existing paths
  330. assert!(path_readable_file(OsStr::new("/bin/sh")).is_ok()); // OK File
  331. assert!(path_readable_file(OsStr::new("/bin/../etc/.././bin/sh")).is_ok()); // Non-canonic.
  332. assert!(path_readable_file(OsStr::new("/../../../../bin/sh")).is_ok()); // Above root
  333. // Inaccessible, nonexistent, or invalid paths
  334. assert!(path_readable_file(OsStr::new("")).is_err()); // Empty String
  335. assert!(path_readable_file(OsStr::new("/")).is_err()); // OK Folder
  336. assert!(path_readable_file(OsStr::new("/etc/shadow")).is_err()); // Denied File
  337. assert!(path_readable_file(OsStr::new("/etc/ssl/private")).is_err()); // Denied Foldr
  338. assert!(path_readable_file(OsStr::new("/nonexistant_test_path")).is_err()); // Missing Path
  339. assert!(path_readable_file(OsStr::new("/null\0containing")).is_err()); // Invalid CStr
  340. }
  341. #[cfg(windows)]
  342. #[test]
  343. fn path_readable_file_basic_functionality() {
  344. unimplemented!("TODO: Pick some appropriate equivalent test paths for Windows");
  345. }
  346. #[cfg(unix)]
  347. #[test]
  348. fn path_readable_file_invalid_utf8() {
  349. assert!(path_readable_file(OsStr::from_bytes(b"/not\xffutf8")).is_err()); // Invalid UTF-8
  350. // TODO: Non-UTF8 path that actually IS valid
  351. }
  352. #[cfg(windows)]
  353. #[test]
  354. fn path_readable_file_unpaired_surrogates() {
  355. assert!(path_readable_file(&OsString::from_wide(
  356. &['C' as u16, ':' as u16, '\\' as u16, 0xd800])).is_err());
  357. // TODO: Unpaired surrogate path that actually IS valid
  358. }
  359. // ---- filename_valid_portable ----
  360. const VALID_FILENAMES: &[&str] = &[
  361. // regular, space, and leading period
  362. "test1", "te st", ".test",
  363. // Stuff which would break if the DOS reserved names check is doing dumb pattern matching
  364. "lpt", "lpt0", "lpt10",
  365. ];
  366. // Paths which should pass because std::path::Path will recognize the separators
  367. // TODO: Actually run the tests on Windows to make sure they work
  368. #[cfg(windows)]
  369. const PATHS_WITH_NATIVE_SEPARATORS: &[&str] = &[
  370. "re/lative", "/ab/solute", "re\\lative", "\\ab\\solute"];
  371. #[cfg(unix)]
  372. const PATHS_WITH_NATIVE_SEPARATORS: &[&str] = &["re/lative", "/ab/solute"];
  373. // Paths which should fail because std::path::Path won't recognize the separators and we don't
  374. // want them showing up in the components.
  375. #[cfg(windows)]
  376. const PATHS_WITH_FOREIGN_SEPARATORS: &[&str] = &["Classic Mac HD:Folder Name:File"];
  377. #[cfg(unix)]
  378. const PATHS_WITH_FOREIGN_SEPARATORS: &[&str] = &[
  379. "relative\\win32",
  380. "C:\\absolute\\win32",
  381. "\\drive\\relative\\win32",
  382. "\\\\unc\\path\\for\\win32",
  383. "Classic Mac HD:Folder Name:File",
  384. ];
  385. // Source: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
  386. const INVALID_PORTABLE_FILENAMES: &[&str] = &[
  387. "test\x03", "test\x07", "test\x08", "test\x0B", "test\x7f", // Control characters (VFAT)
  388. "\"test\"", "<testsss", "testsss>", "testsss|", "testsss*", "testsss?", "?estsss", // VFAT
  389. "ends with space ", "ends_with_period.", // DOS/Win32
  390. "CON", "Con", "coN", "cOn", "CoN", "con", "lpt1", "com9", // Reserved names (DOS/Win32)
  391. "con.txt", "lpt1.dat", // DOS/Win32 API (Reserved names are extension agnostic)
  392. "", "\0"]; // POSIX
  393. #[test]
  394. fn filename_valid_portable_accepts_valid_names() {
  395. for path in VALID_FILENAMES {
  396. assert!(filename_valid_portable(OsStr::new(path)).is_ok(), "{:?}", path);
  397. }
  398. }
  399. #[test]
  400. fn filename_valid_portable_refuses_path_separators() {
  401. for path in PATHS_WITH_NATIVE_SEPARATORS {
  402. assert!(filename_valid_portable(OsStr::new(path)).is_err(), "{:?}", path);
  403. }
  404. for path in PATHS_WITH_FOREIGN_SEPARATORS {
  405. assert!(filename_valid_portable(OsStr::new(path)).is_err(), "{:?}", path);
  406. }
  407. }
  408. #[test]
  409. fn filename_valid_portable_refuses_invalid_characters() {
  410. for fname in INVALID_PORTABLE_FILENAMES {
  411. assert!(filename_valid_portable(OsStr::new(fname)).is_err(), "{:?}", fname);
  412. }
  413. }
  414. #[test]
  415. fn filename_valid_portable_refuses_empty_strings() {
  416. assert!(filename_valid_portable(OsStr::new("")).is_err());
  417. }
  418. #[test]
  419. fn filename_valid_portable_enforces_length_limits() {
  420. // 256 characters
  421. let mut test_str = std::str::from_utf8(&[b'X'; 256]).expect("parsing constant");
  422. assert!(filename_valid_portable(OsStr::new(test_str)).is_err());
  423. // 255 characters (maximum for NTFS, ext2/3/4, and a lot of others)
  424. test_str = std::str::from_utf8(&[b'X'; 255]).expect("parsing constant");
  425. assert!(filename_valid_portable(OsStr::new(test_str)).is_ok());
  426. }
  427. #[cfg(unix)]
  428. #[test]
  429. fn filename_valid_portable_accepts_non_utf8_bytes() {
  430. // Ensure that we don't refuse invalid UTF-8 that "bag of bytes" POSIX allows
  431. assert!(filename_valid_portable(OsStr::from_bytes(b"\xff")).is_ok());
  432. }
  433. #[cfg(windows)]
  434. #[test]
  435. fn filename_valid_portable_accepts_unpaired_surrogates() {
  436. assert!(path_valid_portable(&OsString::from_wide(&[0xd800])).is_ok());
  437. }
  438. // ---- path_valid_portable ----
  439. #[test]
  440. fn path_valid_portable_accepts_valid_names() {
  441. for path in VALID_FILENAMES {
  442. assert!(path_valid_portable(OsStr::new(path)).is_ok(), "{:?}", path);
  443. }
  444. // No filename (.file_stem() returns None)
  445. assert!(path_valid_portable(OsStr::new("foo/..")).is_ok());
  446. }
  447. #[test]
  448. fn path_valid_portable_accepts_native_path_separators() {
  449. for path in PATHS_WITH_NATIVE_SEPARATORS {
  450. assert!(path_valid_portable(OsStr::new(path)).is_ok(), "{:?}", path);
  451. }
  452. // Verify that repeated separators are getting collapsed before filename_valid_portable
  453. // sees them.
  454. // TODO: Make this conditional on platform and also test repeated backslashes on Windows
  455. assert!(path_valid_portable(OsStr::new("/path//with/repeated//separators")).is_ok());
  456. }
  457. #[test]
  458. fn path_valid_portable_refuses_foreign_path_separators() {
  459. for path in PATHS_WITH_FOREIGN_SEPARATORS {
  460. assert!(path_valid_portable(OsStr::new(path)).is_err(), "{:?}", path);
  461. }
  462. }
  463. #[test]
  464. fn path_valid_portable_refuses_invalid_characters() {
  465. for fname in INVALID_PORTABLE_FILENAMES {
  466. assert!(path_valid_portable(OsStr::new(fname)).is_err(), "{:?}", fname);
  467. }
  468. }
  469. #[test]
  470. fn path_valid_portable_enforces_length_limits() {
  471. let mut test_string = String::with_capacity(255 * 130);
  472. #[allow(clippy::decimal_literal_representation)]
  473. while test_string.len() < 32761 {
  474. test_string.push_str(std::str::from_utf8(&[b'X'; 255]).expect("utf8 from literal"));
  475. test_string.push('/');
  476. }
  477. // >32760 characters
  478. assert!(path_valid_portable(OsStr::new(&test_string)).is_err());
  479. // 32760 characters (maximum for FAT32/VFAT/exFAT)
  480. #[allow(clippy::decimal_literal_representation)]
  481. test_string.truncate(32760);
  482. assert!(path_valid_portable(OsStr::new(&test_string)).is_ok());
  483. // 256 characters with no path separators
  484. test_string.truncate(255);
  485. test_string.push('X');
  486. assert!(path_valid_portable(OsStr::new(&test_string)).is_err());
  487. // 255 characters with no path separators
  488. test_string.truncate(255);
  489. assert!(path_valid_portable(OsStr::new(&test_string)).is_ok());
  490. }
  491. #[cfg(unix)]
  492. #[test]
  493. fn path_valid_portable_accepts_non_utf8_bytes() {
  494. // Ensure that we don't refuse invalid UTF-8 that "bag of bytes" POSIX allows
  495. assert!(path_valid_portable(OsStr::from_bytes(b"/\xff/foo")).is_ok());
  496. }
  497. #[cfg(windows)]
  498. #[test]
  499. fn path_valid_portable_accepts_unpaired_surrogates() {
  500. assert!(path_valid_portable(&OsString::from_wide(
  501. &['C' as u16, ':' as u16, '\\' as u16, 0xd800])).is_ok());
  502. }
  503. }