diff --git a/contributors/people.toml b/contributors/people.toml index 405c471a2..fe8e4ddb1 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -209,3 +209,7 @@ sync_github_name = true real_name = "Alexander Buraga" display_name = "Alexander Buraga" sync_github_name = false + +[people.adikshan11] +real_name = "Adithya Shankaran" +sync_github_name = true diff --git a/crates/engine/src/file_access.rs b/crates/engine/src/file_access.rs index 722b854d6..0565a9115 100644 --- a/crates/engine/src/file_access.rs +++ b/crates/engine/src/file_access.rs @@ -11,7 +11,7 @@ //! folder, the rest of the path being plain names (`..` below a missing folder is refused, and so //! is a link that leads nowhere). //! - The result must lie inside one of the roots of its kind, compared by whole components -//! (`/work2` isn't inside `/work`), without regard to case on Windows. +//! (`/work2` isn't inside `/work`), exactly apart from the case of a Windows drive or share prefix. //! - Read and write access are separate: a root given alone grants only its own kind of access. //! //! Roots are in force on a thread inside [`confine`], and in the whole process after @@ -260,7 +260,7 @@ fn resolve(path: &str) -> Result { } /// Is `path` (resolved) `root` or inside it, component by component (without regard to case in a -/// Windows drive prefix only: resolved folder names have their real case, and a folder that differs +/// Windows drive or share prefix only: resolved folder names have their real case, and a folder that differs /// in case alone may be another, on a case-sensitive Windows folder)? fn inside(path: &Path, root: &Path) -> bool { let mut path = path.components(); @@ -280,10 +280,11 @@ fn same_component(a: Component, b: Component) -> bool { } } -/// A Windows device name (`CON`, `NUL`, `COM1`… also with an extension), which names no file. +/// A Windows device name (`CON`, `NUL`, `COM1`, `COM¹`… also with an extension), which names no file. pub(crate) fn is_device_name(component: &str) -> bool { let stem = component.split('.').next().unwrap_or(component).trim_end_matches(' ').to_ascii_uppercase(); - let numbered = |prefix: &str| stem.strip_prefix(prefix).is_some_and(|n| matches!(n, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")); + let numbered = + |prefix: &str| stem.strip_prefix(prefix).is_some_and(|n| matches!(n, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "¹" | "²" | "³")); matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL" | "CLOCK$" | "CONIN$" | "CONOUT$") || numbered("COM") || numbered("LPT") } @@ -499,6 +500,8 @@ mod tests { let e = roots.check(&s(base.join("out/NUL")), Access::Write).unwrap_err(); assert!(e.contains("reserved device names"), "{e}"); assert!(roots.check(&s(base.join("out/com1.txt")), Access::Write).is_err()); + let e = roots.check(&s(base.join("out/COM¹.txt")), Access::Write).unwrap_err(); + assert!(e.contains("reserved device names"), "{e}"); assert_eq!(roots.check(&s(base.join("out/console.txt")), Access::Write), Ok(())); let _ = std::fs::remove_dir_all(base); } @@ -527,10 +530,10 @@ mod tests { #[test] fn device_names() { - for d in ["CON", "nul", "Com1", "LPT9.txt", "aux.svg", "NUL "] { + for d in ["CON", "nul", "Com1", "LPT9.txt", "aux.svg", "NUL ", "COM¹", "Lpt²", "lpt³.txt"] { assert!(is_device_name(d), "{d}"); } - for f in ["console", "com10", "COM0", "lpt", "a.svg", "nul-x"] { + for f in ["console", "com10", "COM0", "lpt", "a.svg", "nul-x", "COM⁴", "com¹0", "lpt¹x"] { assert!(!is_device_name(f), "{f}"); } } diff --git a/crates/engine/src/tests/swatchlib.rs b/crates/engine/src/tests/swatchlib.rs index 7d1f77cd7..18d2ecd33 100644 --- a/crates/engine/src/tests/swatchlib.rs +++ b/crates/engine/src/tests/swatchlib.rs @@ -769,9 +769,17 @@ fn a_pattern_library_adds_only_the_picked_pattern_and_it_undoes_and_saves() { #[test] fn a_library_file_name_is_never_a_windows_device_name() { use crate::cmd::swatchlib::library_file_name; - for (title, ext) in - [("CON", "json"), ("nul", "json"), ("Com1", "acb"), ("LPT9", "ase"), ("aux.", "json"), ("CON.txt", "json"), ("nul.tar.gz", "acb")] - { + for (title, ext) in [ + ("CON", "json"), + ("nul", "json"), + ("Com1", "acb"), + ("LPT9", "ase"), + ("aux.", "json"), + ("CON.txt", "json"), + ("nul.tar.gz", "acb"), + ("COM¹", "json"), + ("lpt³.x", "ase"), + ] { let file = library_file_name(title, ext); assert!(!crate::file_access::is_device_name(&file), "{title}: {file}"); assert!( diff --git a/docs/mcp.md b/docs/mcp.md index 31e64ed56..6400a9099 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4508,7 +4508,7 @@ row's files in a sub-folder (its size or format, or its own `folder`; `folders: sub-folders) replaces the rows, and `settings: {png: {…}, jpg: {…}, svg: {…}, pdf: {preset}}` gives every row of a format its options. On every platform, `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters from a prefix, a suffix, a name pattern or an item's name become `-` in file and folder names, a folder name ends without -periods or spaces, and a Windows device name (`CON`, `PRN`, `AUX`, `NUL`, `COM1` to `COM9`, `LPT1` to `LPT9`) that +periods or spaces, and a Windows device name (`CON`, `PRN`, `AUX`, `NUL`, `COM1` to `COM9`, `COM¹` to `COM³`, `LPT1` to `LPT9`, `LPT¹` to `LPT³`) that is the whole name or comes before its first period gets a `-` after it (`CON-.png`). When a row's file would take a name another row's file already has, compared without case (two PNG rows with different backgrounds and no suffix), the later file is written as `-2`, `-3`… (`Artboard-1-2.png`) and listed under that name in `files`; two identical