From df9d744ef464d4df7db39812ab1631da74dc15e8 Mon Sep 17 00:00:00 2001 From: Gavin Panella Date: Mon, 20 Oct 2025 14:46:30 +0200 Subject: [PATCH 1/3] Provide `impl TryFrom` for OS strings and paths on Windows --- .github/workflows/build.yml | 26 +++++++- README.md | 2 +- src/lib.rs | 116 ++++++++++++++++++++++++++++-------- 3 files changed, 114 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 47d6145..c63a549 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,16 +8,25 @@ on: jobs: test: name: Test - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable # Make sure we have all the target shells installed. - - run: sudo apt-get install -y bash dash fish zsh + - name: Install shells (Ubuntu) + if: runner.os == 'Linux' + run: sudo apt-get install -y bash dash fish zsh + - name: Install shells (Windows) + if: runner.os == 'Windows' + run: choco install git -y --no-progress # `git` includes `bash`. # Record shell versions. Almost unbelievably, it's not possible to get the # version of Dash from `dash` itself, so we skip it here. Since `sh` might # be `dash`, we also do not try to get its version. - - name: Shell versions + - name: Shell versions (Ubuntu) + if: runner.os == 'Linux' run: | for shell in sh dash; do for path in $(type -ap "$shell"); do @@ -30,6 +39,17 @@ jobs: printf "%10s @ %-30q: %s\n" "$shell" "$path" "$version" done done + - name: Shell versions (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + for shell in bash; do + if command -v "$shell" &> /dev/null; then + path=$(command -v "$shell") + version=$("$shell" --version | head -n 1) + printf "%10s @ %-30s: %s\n" "$shell" "$path" "$version" + fi + done # Test in debug mode first. - run: cargo test # Test in release mode too, to defend against, for example, use of diff --git a/README.md b/README.md index 6025cc5..e5ef50e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ It can take as input many different string and byte string types: - [`&str`] and [`String`] - [`&bstr::BStr`][`bstr::BStr`] and [`bstr::BString`] - [`&[u8]`][`slice`] and [`Vec`] -- [`&OsStr`][`OsStr`] and [`OsString`] (on UNIX) +- [`&OsStr`][`OsStr`] and [`OsString`] - [`&Path`][`Path`] and [`PathBuf`] and produce output as (or push into) the following types: diff --git a/src/lib.rs b/src/lib.rs index a1504ec..c8b65af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,9 +36,6 @@ doc = include_str!("../README.md") )] -use std::ffi::{OsStr, OsString}; -use std::path::{Path, PathBuf}; - mod ascii; mod bash; mod fish; @@ -176,22 +173,6 @@ impl<'a> From<&'a String> for Quotable<'a> { } } -#[cfg(unix)] -impl<'a> From<&'a OsStr> for Quotable<'a> { - fn from(source: &'a OsStr) -> Quotable<'a> { - use std::os::unix::ffi::OsStrExt; - source.as_bytes().into() - } -} - -#[cfg(unix)] -impl<'a> From<&'a OsString> for Quotable<'a> { - fn from(source: &'a OsString) -> Quotable<'a> { - use std::os::unix::ffi::OsStrExt; - source.as_bytes().into() - } -} - #[cfg(feature = "bstr")] impl<'a> From<&'a bstr::BStr> for Quotable<'a> { fn from(source: &'a bstr::BStr) -> Quotable<'a> { @@ -208,16 +189,99 @@ impl<'a> From<&'a bstr::BString> for Quotable<'a> { } } +// ---------------------------------------------------------------------------- + +/// Unix-specific support. #[cfg(unix)] -impl<'a> From<&'a Path> for Quotable<'a> { - fn from(source: &'a Path) -> Quotable<'a> { - source.as_os_str().into() +mod platform { + use std::ffi::{OsStr, OsString}; + use std::path::{Path, PathBuf}; + + use super::Quotable; + + impl<'a> From<&'a OsStr> for Quotable<'a> { + fn from(source: &'a OsStr) -> Quotable<'a> { + use std::os::unix::ffi::OsStrExt; + source.as_bytes().into() + } + } + + impl<'a> From<&'a OsString> for Quotable<'a> { + fn from(source: &'a OsString) -> Quotable<'a> { + use std::os::unix::ffi::OsStrExt; + source.as_bytes().into() + } + } + + impl<'a> From<&'a Path> for Quotable<'a> { + fn from(source: &'a Path) -> Quotable<'a> { + source.as_os_str().into() + } + } + + impl<'a> From<&'a PathBuf> for Quotable<'a> { + fn from(source: &'a PathBuf) -> Quotable<'a> { + source.as_os_str().into() + } } } -#[cfg(unix)] -impl<'a> From<&'a PathBuf> for Quotable<'a> { - fn from(source: &'a PathBuf) -> Quotable<'a> { - source.as_os_str().into() +// ---------------------------------------------------------------------------- + +#[cfg(windows)] +pub use platform::UnicodeError; + +/// Windows-specific support. +#[cfg(windows)] +mod platform { + use std::ffi::{OsStr, OsString}; + use std::path::{Path, PathBuf}; + + use super::Quotable; + + /// Error when converting an [`OsStr`]/[`OsString`] that contains invalid + /// Unicode, e.g. unpaired UTF-16 surrogates on Windows. + #[derive(Debug, Clone)] + #[non_exhaustive] + pub struct UnicodeError {} + + impl std::error::Error for UnicodeError {} + + impl std::fmt::Display for UnicodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "invalid Unicode (unpaired UTF-16 surrogates)") + } + } + + impl<'a> TryFrom<&'a OsStr> for Quotable<'a> { + type Error = UnicodeError; + + fn try_from(source: &'a OsStr) -> Result { + source.to_str().map(Quotable::Text).ok_or(UnicodeError {}) + } + } + + impl<'a> TryFrom<&'a OsString> for Quotable<'a> { + type Error = UnicodeError; + + fn try_from(source: &'a OsString) -> Result { + source.as_os_str().try_into() + } + } + + impl<'a> TryFrom<&'a Path> for Quotable<'a> { + type Error = UnicodeError; + + fn try_from(source: &'a Path) -> Result { + source.as_os_str().try_into() + } + } + + impl<'a> TryFrom<&'a PathBuf> for Quotable<'a> { + type Error = UnicodeError; + + fn try_from(source: &'a PathBuf) -> Result { + source.as_os_str().try_into() + } } } From 791833d60d387324e32049482651851addd533e6 Mon Sep 17 00:00:00 2001 From: Gavin Panella Date: Wed, 22 Oct 2025 11:37:19 +0200 Subject: [PATCH 2/3] Upgrade dev dependencies --- Cargo.toml | 6 +++--- benches/bash.rs | 4 +++- benches/fish.rs | 4 +++- benches/sh.rs | 4 +++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a19f692..3391520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,9 @@ sh = [] bstr = { version = "1", optional = true } [dev-dependencies] -criterion = { version = "^0.5.1", features = ["html_reports"] } -lenient_semver = "0.4.2" -semver = "1.0.23" +criterion = { version = "0.7", features = ["html_reports"] } +lenient_semver = "0.4" +semver = "1.0" test-case = "3.3.1" [[bench]] diff --git a/benches/bash.rs b/benches/bash.rs index e0cf0c6..6474c33 100644 --- a/benches/bash.rs +++ b/benches/bash.rs @@ -1,4 +1,6 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, Criterion}; use shell_quote::Bash; diff --git a/benches/fish.rs b/benches/fish.rs index dc50d93..ae6612e 100644 --- a/benches/fish.rs +++ b/benches/fish.rs @@ -1,4 +1,6 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, Criterion}; use shell_quote::Fish; diff --git a/benches/sh.rs b/benches/sh.rs index 2ff96df..fb263ce 100644 --- a/benches/sh.rs +++ b/benches/sh.rs @@ -1,4 +1,6 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, Criterion}; use shell_quote::Sh; From d774edb959626db895c3e913d91394bb57d418ce Mon Sep 17 00:00:00 2001 From: Gavin Panella Date: Wed, 22 Oct 2025 20:26:13 +0200 Subject: [PATCH 3/3] Use `OsStr::as_encoded_bytes` et al. i.e. `OsString::from_encoded_bytes_unchecked` too, where it's safe. `OsStr`/`OsString` are always encoded as a superset of UTF-8, on all platforms. For shells where we can guarantee UTF-8 output, i.e. `bash`/`zsh` and `fish`, we can use these cross-platform methods. For `sh`, where we cannot guarantee UTF-8, then `OsStr`/`OsString` is only available on Unix platforms. --- Makefile | 5 ++ README.md | 2 +- src/bash.rs | 5 +- src/fish.rs | 5 +- src/lib.rs | 119 ++++++++++----------------------------------- tests/test_bash.rs | 28 ++++------- tests/test_fish.rs | 48 ++++++++---------- tests/test_ux.rs | 58 +++++++++++++--------- 8 files changed, 100 insertions(+), 170 deletions(-) diff --git a/Makefile b/Makefile index 36c3d10..a2f92b6 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ .PHONY: test test: python3 test.py | bash -xe + +.PHONY: check +check: + cargo check --all-targets --all-features + cargo check --all-targets --all-features --target aarch64-pc-windows-msvc diff --git a/README.md b/README.md index e5ef50e..07e3970 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ and produce output as (or push into) the following types: - [`String`] (for shells that support it, i.e. not [`Sh`]/[`Dash`]) - [`bstr::BString`] - [`Vec`] -- [`OsString`] (on UNIX) +- [`OsString`] (except for [`Sh`]) Inspired by the Haskell [shell-escape][] package. diff --git a/src/bash.rs b/src/bash.rs index 63ba917..9b7a82c 100644 --- a/src/bash.rs +++ b/src/bash.rs @@ -81,12 +81,11 @@ impl QuoteInto for Bash { } } -#[cfg(unix)] impl QuoteInto for Bash { fn quote_into<'q, S: Into>>(s: S, out: &mut std::ffi::OsString) { - use std::os::unix::ffi::OsStringExt; let s = Self::quote_vec(s); - let s = std::ffi::OsString::from_vec(s); + // SAFETY: `Bash::quote_vec` produces UTF-8. + let s = unsafe { std::ffi::OsString::from_encoded_bytes_unchecked(s) }; out.push(s); } } diff --git a/src/fish.rs b/src/fish.rs index 647cbcd..1212e20 100644 --- a/src/fish.rs +++ b/src/fish.rs @@ -53,12 +53,11 @@ impl QuoteInto for Fish { } } -#[cfg(unix)] impl QuoteInto for Fish { fn quote_into<'q, S: Into>>(s: S, out: &mut std::ffi::OsString) { - use std::os::unix::ffi::OsStringExt; let s = Self::quote_vec(s); - let s = std::ffi::OsString::from_vec(s); + // SAFETY: `Fish::quote_vec` produces UTF-8. + let s = unsafe { std::ffi::OsString::from_encoded_bytes_unchecked(s) }; out.push(s); } } diff --git a/src/lib.rs b/src/lib.rs index c8b65af..2588102 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,11 @@ mod fish; mod sh; mod utf8; +use std::{ + ffi::{OsStr, OsString}, + path::{Path, PathBuf}, +}; + #[cfg(feature = "bash")] pub use bash::Bash; #[cfg(feature = "fish")] @@ -84,7 +89,8 @@ impl, OUT: Default> Quote for T {} /// Extension trait for pushing shell quoted byte slices, e.g. `&[u8]`, [`&str`] /// – anything that's [`Quotable`] – into container types like [`Vec`], -/// [`String`], [`OsString`] on Unix, and [`bstr::BString`] if it's enabled. +/// [`String`], [`OsString`] where possible, and [`bstr::BString`] if it's +/// enabled. pub trait QuoteExt { fn push_quoted<'q, Q, S>(&mut self, _q: Q, s: S) where @@ -106,7 +112,7 @@ impl QuoteExt for T { /// Extension trait for shell quoting many different owned and reference types, /// e.g. `&[u8]`, [`&str`] – anything that's [`Quotable`] – into owned container -/// types like [`Vec`], [`String`], [`OsString`] on Unix, and +/// types like [`Vec`], [`String`], [`OsString`] where possible, and /// [`bstr::BString`] if it's enabled. pub trait QuoteRefExt { fn quoted>(self, q: Q) -> Output; @@ -173,6 +179,18 @@ impl<'a> From<&'a String> for Quotable<'a> { } } +impl<'a> From<&'a OsStr> for Quotable<'a> { + fn from(source: &'a OsStr) -> Quotable<'a> { + source.as_encoded_bytes().into() + } +} + +impl<'a> From<&'a OsString> for Quotable<'a> { + fn from(source: &'a OsString) -> Quotable<'a> { + source.as_encoded_bytes().into() + } +} + #[cfg(feature = "bstr")] impl<'a> From<&'a bstr::BStr> for Quotable<'a> { fn from(source: &'a bstr::BStr) -> Quotable<'a> { @@ -189,99 +207,14 @@ impl<'a> From<&'a bstr::BString> for Quotable<'a> { } } -// ---------------------------------------------------------------------------- - -/// Unix-specific support. -#[cfg(unix)] -mod platform { - use std::ffi::{OsStr, OsString}; - use std::path::{Path, PathBuf}; - - use super::Quotable; - - impl<'a> From<&'a OsStr> for Quotable<'a> { - fn from(source: &'a OsStr) -> Quotable<'a> { - use std::os::unix::ffi::OsStrExt; - source.as_bytes().into() - } - } - - impl<'a> From<&'a OsString> for Quotable<'a> { - fn from(source: &'a OsString) -> Quotable<'a> { - use std::os::unix::ffi::OsStrExt; - source.as_bytes().into() - } - } - - impl<'a> From<&'a Path> for Quotable<'a> { - fn from(source: &'a Path) -> Quotable<'a> { - source.as_os_str().into() - } - } - - impl<'a> From<&'a PathBuf> for Quotable<'a> { - fn from(source: &'a PathBuf) -> Quotable<'a> { - source.as_os_str().into() - } +impl<'a> From<&'a Path> for Quotable<'a> { + fn from(source: &'a Path) -> Quotable<'a> { + source.as_os_str().into() } } -// ---------------------------------------------------------------------------- - -#[cfg(windows)] -pub use platform::UnicodeError; - -/// Windows-specific support. -#[cfg(windows)] -mod platform { - use std::ffi::{OsStr, OsString}; - use std::path::{Path, PathBuf}; - - use super::Quotable; - - /// Error when converting an [`OsStr`]/[`OsString`] that contains invalid - /// Unicode, e.g. unpaired UTF-16 surrogates on Windows. - #[derive(Debug, Clone)] - #[non_exhaustive] - pub struct UnicodeError {} - - impl std::error::Error for UnicodeError {} - - impl std::fmt::Display for UnicodeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "invalid Unicode (unpaired UTF-16 surrogates)") - } - } - - impl<'a> TryFrom<&'a OsStr> for Quotable<'a> { - type Error = UnicodeError; - - fn try_from(source: &'a OsStr) -> Result { - source.to_str().map(Quotable::Text).ok_or(UnicodeError {}) - } - } - - impl<'a> TryFrom<&'a OsString> for Quotable<'a> { - type Error = UnicodeError; - - fn try_from(source: &'a OsString) -> Result { - source.as_os_str().try_into() - } - } - - impl<'a> TryFrom<&'a Path> for Quotable<'a> { - type Error = UnicodeError; - - fn try_from(source: &'a Path) -> Result { - source.as_os_str().try_into() - } - } - - impl<'a> TryFrom<&'a PathBuf> for Quotable<'a> { - type Error = UnicodeError; - - fn try_from(source: &'a PathBuf) -> Result { - source.as_os_str().try_into() - } +impl<'a> From<&'a PathBuf> for Quotable<'a> { + fn from(source: &'a PathBuf) -> Quotable<'a> { + source.as_os_str().into() } } diff --git a/tests/test_bash.rs b/tests/test_bash.rs index f1f00f5..e0c0688 100644 --- a/tests/test_bash.rs +++ b/tests/test_bash.rs @@ -81,41 +81,36 @@ mod bash_impl { assert_eq!(buffer, b"$'-_=/,.+'"); } - #[cfg(unix)] #[test_matrix( (script_bytes, script_text), ("bash", "zsh") )] fn test_roundtrip(prepare: fn() -> (OsString, OsString), shell: &str) { - use std::os::unix::ffi::OsStringExt; let (input, script) = prepare(); for bin in find_bins(shell) { let output = invoke_shell(&bin, &script).unwrap(); - let result = OsString::from_vec(output.stdout); + let result = unsafe { OsString::from_encoded_bytes_unchecked(output.stdout) }; assert_eq!(result, input); } } - #[cfg(unix)] fn script_bytes() -> (OsString, OsString) { - use std::os::unix::ffi::{OsStrExt, OsStringExt}; // It doesn't seem possible to roundtrip NUL, probably because it is the // string terminator character in C. - let input: OsString = OsString::from_vec((1..=u8::MAX).collect()); + let input: OsString = + unsafe { OsString::from_encoded_bytes_unchecked((1..=u8::MAX).collect()) }; // NOTE: Do NOT use `echo` here; in most/all shells it interprets // escapes with no way to disable that behaviour (unlike the `echo` // builtin in Bash, for example, which accepts a `-E` flag). Using // `printf %s` seems to do the right thing in most shells, i.e. it does // not interpret the arguments in any way. let mut script = b"printf %s ".to_vec(); - Bash::quote_into_vec(input.as_bytes(), &mut script); - let script = OsString::from_vec(script); + Bash::quote_into_vec(input.as_encoded_bytes(), &mut script); + let script = unsafe { OsString::from_encoded_bytes_unchecked(script) }; (input, script) } - #[cfg(unix)] fn script_text() -> (OsString, OsString) { - use std::os::unix::ffi::OsStringExt; // NOTE: Do NOT use `echo` here; in most/all shells it interprets // escapes with no way to disable that behaviour (unlike the `echo` // builtin in Bash, for example, which accepts a `-E` flag). Using @@ -123,14 +118,12 @@ mod bash_impl { // not interpret the arguments in any way. let mut script = b"printf %s ".to_vec(); Bash::quote_into_vec(resources::UTF8_SAMPLE, &mut script); - let script = OsString::from_vec(script); + let script = unsafe { OsString::from_encoded_bytes_unchecked(script) }; (resources::UTF8_SAMPLE.into(), script) } - #[cfg(unix)] #[test_matrix(("bash", "zsh"))] fn test_roundtrip_utf8_full(shell: &str) { - use std::os::unix::ffi::OsStringExt; let utf8: Vec<_> = ('\x01'..=char::MAX).collect(); // Not including NUL. for bin in find_bins(shell) { // Chunk to avoid over-length arguments (see`getconf ARG_MAX`). @@ -138,9 +131,9 @@ mod bash_impl { let input: String = String::from_iter(chunk); let mut script = b"printf %s ".to_vec(); Bash::quote_into_vec(&input, &mut script); - let script = OsString::from_vec(script); + let script = unsafe { OsString::from_encoded_bytes_unchecked(script) }; let output = invoke_shell(&bin, &script).unwrap(); - let observed = OsString::from_vec(output.stdout); + let observed = unsafe { OsString::from_encoded_bytes_unchecked(output.stdout) }; assert_eq!(observed.into_string(), Ok(input)); } } @@ -160,12 +153,9 @@ mod bash_quote_ext { assert_eq!(string, "Hello, $'World, Bob, !@#$%^&*(){}[]'"); } - #[cfg(unix)] #[test] fn test_os_string_push_quoted_with_bash() { - use std::ffi::OsString; - - let mut buffer: OsString = "Hello, ".into(); + let mut buffer: std::ffi::OsString = "Hello, ".into(); buffer.push_quoted(Bash, "World, Bob, !@#$%^&*(){}[]"); let string = buffer.into_string().unwrap(); // -> test failures are more readable. assert_eq!(string, "Hello, $'World, Bob, !@#$%^&*(){}[]'"); diff --git a/tests/test_fish.rs b/tests/test_fish.rs index 4c0e027..5f02964 100644 --- a/tests/test_fish.rs +++ b/tests/test_fish.rs @@ -128,48 +128,41 @@ mod fish_impl { assert_eq!(buffer, b"-_'=/,.+'"); } - #[cfg(unix)] #[test_matrix((script_bytes, script_text))] fn test_roundtrip(prepare: fn() -> (OsString, OsString)) { - use std::os::unix::ffi::OsStringExt; let (input, script) = prepare(); // Test with every version of `fish` we find on `PATH`. for bin in find_bins("fish") { let output = invoke_shell(&bin, &script).unwrap(); - let result = OsString::from_vec(output.stdout); + let result = unsafe { OsString::from_encoded_bytes_unchecked(output.stdout) }; assert_eq!(result, input); } } - #[cfg(unix)] fn script_bytes() -> (OsString, OsString) { - use std::os::unix::ffi::{OsStrExt, OsStringExt}; // It doesn't seem possible to roundtrip NUL, probably because it is the // string terminator character in C. - let input: OsString = OsString::from_vec((1..=u8::MAX).collect()); + let input: OsString = + unsafe { OsString::from_encoded_bytes_unchecked((1..=u8::MAX).collect()) }; // Unlike many/most other shells, `echo` is safe here because backslash // escapes are _not_ interpreted by default. let mut script = b"echo -n -- ".to_vec(); - Fish::quote_into_vec(input.as_bytes(), &mut script); - let script = OsString::from_vec(script); + Fish::quote_into_vec(input.as_encoded_bytes(), &mut script); + let script = unsafe { OsString::from_encoded_bytes_unchecked(script) }; (input, script) } - #[cfg(unix)] fn script_text() -> (OsString, OsString) { - use std::os::unix::ffi::OsStringExt; // Unlike many/most other shells, `echo` is safe here because backslash // escapes are _not_ interpreted by default. let mut script = b"echo -n -- ".to_vec(); Fish::quote_into_vec(resources::UTF8_SAMPLE, &mut script); - let script = OsString::from_vec(script); + let script = unsafe { OsString::from_encoded_bytes_unchecked(script) }; (resources::UTF8_SAMPLE.into(), script) } - #[cfg(unix)] #[test] fn test_roundtrip_utf8_full() { - use std::os::unix::ffi::OsStringExt; let utf8: Vec<_> = ('\x01'..=char::MAX).collect(); // Not including NUL. for bin in find_bins("fish") { let version = super::fish_version(&bin); @@ -182,20 +175,18 @@ mod fish_impl { let input: String = String::from_iter(chunk); let mut script = b"printf %s ".to_vec(); Fish::quote_into_vec(&input, &mut script); - let script = OsString::from_vec(script); + let script = unsafe { OsString::from_encoded_bytes_unchecked(script) }; let output = invoke_shell(&bin, &script).unwrap(); - let observed = OsString::from_vec(output.stdout); + let observed = unsafe { OsString::from_encoded_bytes_unchecked(output.stdout) }; assert_eq!(observed.into_string(), Ok(input)); } } } - #[cfg(unix)] #[test] /// IIRC, this caught bugs not found by `test_roundtrip_utf8_full`, and it /// was much easier to figure out what the failures meant. For now it stays! fn test_roundtrip_utf8_full_char_by_char() { - use std::os::unix::ffi::OsStringExt; let utf8: Vec<_> = ('\x01'..=char::MAX).collect(); // Not including NUL. for bin in find_bins("fish") { let version = super::fish_version(&bin); @@ -205,14 +196,16 @@ mod fish_impl { } // Chunk to avoid over-length arguments (see`getconf ARG_MAX`). for chunk in utf8.chunks(2usize.pow(12)) { - let script = OsString::from_vec(chunk.iter().fold( - b"printf '%s\\0'".to_vec(), - |mut script, ch| { - script.push(b' '); - Fish::quote_into_vec(&ch.to_string(), &mut script); - script - }, - )); + let script = unsafe { + OsString::from_encoded_bytes_unchecked(chunk.iter().fold( + b"printf '%s\\0'".to_vec(), + |mut script, ch| { + script.push(b' '); + Fish::quote_into_vec(&ch.to_string(), &mut script); + script + }, + )) + }; let output = invoke_shell(&bin, &script).unwrap(); let observed = output.stdout.split(|ch| *ch == 0).filter(|s| !s.is_empty()); @@ -242,12 +235,9 @@ mod fish_quote_ext { assert_eq!(string, "Hello, World,' Bob, !@#$%^&*(){}[]'"); } - #[cfg(unix)] #[test] fn test_os_string_push_quoted_with_fish() { - use std::ffi::OsString; - - let mut buffer: OsString = "Hello, ".into(); + let mut buffer: std::ffi::OsString = "Hello, ".into(); buffer.push_quoted(Fish, "World, Bob, !@#$%^&*(){}[]"); let string = buffer.into_string().unwrap(); // -> test failures are more readable. assert_eq!(string, "Hello, World,' Bob, !@#$%^&*(){}[]'"); diff --git a/tests/test_ux.rs b/tests/test_ux.rs index 646fed6..717af42 100644 --- a/tests/test_ux.rs +++ b/tests/test_ux.rs @@ -1,9 +1,24 @@ -#![cfg(all(unix, feature = "bash", feature = "bstr"))] +#![cfg(all(any(unix, windows), feature = "bash", feature = "bstr"))] use bstr::{BString, ByteSlice}; -use std::{ffi::OsString, os::unix::ffi::OsStringExt}; +use std::ffi::OsString; -use shell_quote::{Bash, Quotable, QuoteRefExt}; +#[cfg(unix)] +use std::os::unix::ffi::OsStringExt; + +use shell_quote::Quotable; + +#[cfg(unix)] +use shell_quote::{Bash, QuoteRefExt}; + +// ----------------------------------------------------------------------------- + +/// Convert `T` into a `Quotable`. +fn into_quotable<'a, T: Into>>(source: T) -> Quotable<'a> { + source.into() +} + +// ----------------------------------------------------------------------------- #[test] fn test_quotable_conversions() { @@ -11,35 +26,24 @@ fn test_quotable_conversions() { let byte_slice = &b"bytes"[..]; let vec = Vec::from(b"vec"); let os_string = OsString::from("os-string"); - let b_string = bstr::BString::from(b"b-string"); + let b_string = BString::from(b"b-string"); let path_buf = std::path::PathBuf::from("/path/[to]/file"); let string = "string".to_owned(); - let _: Quotable = bytes.into(); - let _: Quotable = byte_slice.into(); - let _: Quotable = (&vec).into(); - let _: Quotable = (&os_string).into(); - let _: Quotable = os_string.as_os_str().into(); - let _: Quotable = (&b_string).into(); - let _: Quotable = b_string.as_bstr().into(); - let _: Quotable = (&path_buf).into(); - let _: Quotable = path_buf.as_path().into(); - let _: Quotable = (&string).into(); - let _: Quotable = string.as_str().into(); - - fn into_quotable<'a, T: Into>>(source: T) -> Quotable<'a> { - source.into() - } - into_quotable(bytes); into_quotable(byte_slice); into_quotable(&vec); - into_quotable(&os_string); into_quotable(&b_string); - into_quotable(&path_buf); + into_quotable(b_string.as_bstr()); into_quotable(&string); + into_quotable(string.as_str()); + into_quotable(&os_string); + into_quotable(os_string.as_os_str()); + into_quotable(&path_buf); + into_quotable(path_buf.as_path()); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_byte_array() { let source = b"bytes!"; @@ -53,6 +57,7 @@ fn test_quote_ref_ext_byte_array() { assert_eq!(BString::from(b"$'bytes!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_byte_slice() { let source = &b"bytes!"[..]; @@ -66,6 +71,7 @@ fn test_quote_ref_ext_byte_slice() { assert_eq!(BString::from(b"$'bytes!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_vec() { let source = Vec::from(b"vec!"); @@ -79,6 +85,7 @@ fn test_quote_ref_ext_vec() { assert_eq!(BString::from(b"$'vec!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_os_string() { let source = OsString::from("os-string!"); @@ -92,6 +99,7 @@ fn test_quote_ref_ext_os_string() { assert_eq!(BString::from(b"$'os-string!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_os_str() { let source = OsString::from("os-str!"); @@ -106,6 +114,7 @@ fn test_quote_ref_ext_os_str() { assert_eq!(BString::from(b"$'os-str!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_b_string() { let source = bstr::BString::from(b"b-string!"); @@ -119,6 +128,7 @@ fn test_quote_ref_ext_b_string() { assert_eq!(BString::from(b"$'b-string!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_b_str() { let source = bstr::BString::from(b"b-str!"); @@ -133,6 +143,7 @@ fn test_quote_ref_ext_b_str() { assert_eq!(BString::from(b"$'b-str!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_path_buf() { let source = std::path::PathBuf::from("path-buf!"); @@ -146,6 +157,7 @@ fn test_quote_ref_ext_path_buf() { assert_eq!(BString::from(b"$'path-buf!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_path() { let source = std::path::PathBuf::from("path!"); @@ -160,6 +172,7 @@ fn test_quote_ref_ext_path() { assert_eq!(BString::from(b"$'path!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_string() { let source = "string!".to_owned(); @@ -173,6 +186,7 @@ fn test_quote_ref_ext_string() { assert_eq!(BString::from(b"$'string!'"), quoted); } +#[cfg(unix)] #[test] fn test_quote_ref_ext_str() { let source = "str!";