use std::ffi::{OsStr, OsString};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[derive(Debug)]
pub struct NonUtf8OsStrError {
input_lossy_string: String,
}
impl NonUtf8OsStrError {
#[cfg(test)]
pub(crate) fn new_for_test(input_lossy_string: impl Into<String>) -> Self {
Self {
input_lossy_string: input_lossy_string.into(),
}
}
}
impl std::fmt::Display for NonUtf8OsStrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use os_display::Quotable;
let quoted = self.input_lossy_string.quote();
write!(f, "invalid UTF-8 input {quoted}")
}
}
impl std::error::Error for NonUtf8OsStrError {}
pub trait UError: std::error::Error {}
#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))]
pub fn os_str_as_bytes(os_string: &OsStr) -> Result<&[u8], NonUtf8OsStrError> {
#[cfg(unix)]
return Ok(os_string.as_bytes());
#[cfg(target_os = "wasi")]
return Ok(os_string.as_encoded_bytes());
#[cfg(not(any(unix, target_os = "wasi")))]
os_string
.to_str()
.ok_or_else(|| NonUtf8OsStrError {
input_lossy_string: os_string.to_string_lossy().into_owned(),
})
.map(str::as_bytes)
}
#[allow(clippy::needless_pass_by_value)]
pub fn set_exit_code(_code: i32) {}
pub fn strip_errno(err: &std::io::Error) -> String {
let mut msg = err.to_string();
if let Some(pos) = msg.find(" (os error ") {
msg.truncate(pos);
}
msg
}
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug)]
pub enum QuotingStyle {
C_NO_QUOTES,
Escape,
SHELL_ESCAPE,
}
pub const DIAGNOSTIC_QUOTING_STYLE: QuotingStyle = QuotingStyle::Escape;
pub fn locale_aware_escape_name(input: &OsStr, style: QuotingStyle) -> OsString {
match style {
QuotingStyle::C_NO_QUOTES | QuotingStyle::Escape => input.to_os_string(),
QuotingStyle::SHELL_ESCAPE => shell_quote(&input.to_string_lossy()).into(),
}
}
pub fn locale_aware_shell_escape(input: impl AsRef<OsStr>) -> String {
locale_aware_escape_name(input.as_ref(), QuotingStyle::SHELL_ESCAPE)
.into_string()
.unwrap_or_else(|os| os.to_string_lossy().into_owned())
}
const MAX_COLLECTED_DIAGNOSTICS: usize = 64;
std::thread_local! {
static DIAGNOSTICS: std::cell::RefCell<Option<Vec<String>>> =
const { std::cell::RefCell::new(None) };
}
pub fn collect_diagnostics<R>(f: impl FnOnce() -> R) -> (R, Vec<String>) {
DIAGNOSTICS.with(|d| *d.borrow_mut() = Some(Vec::new()));
let result = f();
let diags = DIAGNOSTICS
.with(|d| d.borrow_mut().take())
.unwrap_or_default();
(result, diags)
}
pub fn record_diagnostic(msg: std::fmt::Arguments<'_>) {
DIAGNOSTICS.with(|d| {
if let Some(diags) = d.borrow_mut().as_mut()
&& diags.len() < MAX_COLLECTED_DIAGNOSTICS
{
diags.push(msg.to_string());
}
});
}
macro_rules! show_error {
($($arg:tt)*) => {{
$crate::builtins::generated::format_support::record_diagnostic(format_args!($($arg)*));
}};
}
macro_rules! show_warning {
($($arg:tt)*) => {{
let _ = format_args!($($arg)*);
}};
}
pub(crate) use show_error;
pub(crate) use show_warning;
fn shell_quote(s: &str) -> String {
if s.is_empty() {
return "''".to_string();
}
let needs_escape = |prev: Option<char>, c: char| match c {
'#' => prev.is_none(),
'~' => matches!(prev, None | Some('=' | ':')),
c if c.is_ascii() => !(c.is_ascii_alphanumeric() || "_/.:-=+@%".contains(c)),
c => c.is_control(),
};
let escapes = || {
let mut prev = None;
s.chars().map(move |c| {
let e = needs_escape(prev, c);
prev = Some(c);
(c, e)
})
};
if !escapes().any(|(_, e)| e) {
return s.to_string();
}
let has_control = s.chars().any(char::is_control);
if has_control {
let mut out = String::from("$'");
for ch in s.chars() {
match ch {
'\'' => out.push_str("\\'"),
'\\' => out.push_str("\\\\"),
'\x07' => out.push_str("\\a"),
'\x08' => out.push_str("\\b"),
'\x1b' => out.push_str("\\E"),
'\x0c' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
'\x0b' => out.push_str("\\v"),
c if c.is_control() => {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).bytes() {
out.push_str(&format!("\\{b:03o}"));
}
}
c => out.push(c),
}
}
out.push('\'');
return out;
}
let mut out = String::new();
for (ch, escape) in escapes() {
if escape {
out.push('\\');
}
out.push(ch);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_and_c_no_quotes_escape_identically() {
for input in ["", "abc", "a\tb", "a b", "quote'd", "\u{1b}[0m"] {
let os = OsStr::new(input);
assert_eq!(
locale_aware_escape_name(os, QuotingStyle::C_NO_QUOTES),
locale_aware_escape_name(os, QuotingStyle::Escape),
"styles disagree on {input:?}" );
}
}
#[test]
fn shell_escape_shorthand_matches_escape_name() {
for input in ["", "plain", "hello world", "a'b", "tab\there"] {
assert_eq!(
locale_aware_shell_escape(input),
locale_aware_escape_name(OsStr::new(input), QuotingStyle::SHELL_ESCAPE)
.to_string_lossy(),
"shorthand diverged on {input:?}" );
}
}
#[test]
fn generated_module_quotes_diagnostics_with_the_style_printf_expects() {
const ARGUMENT_RS: &str = include_str!("format/argument.rs");
let probe = OsStr::new("a\tb c'd");
let expected = locale_aware_escape_name(probe, DIAGNOSTIC_QUOTING_STYLE);
let named: Vec<&str> = ARGUMENT_RS
.match_indices("QuotingStyle::")
.map(|(i, _)| {
let rest = &ARGUMENT_RS[i + "QuotingStyle::".len()..];
let end = rest
.find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.unwrap_or(rest.len());
&rest[..end]
})
.collect();
assert!(
!named.is_empty(),
"generated/format/argument.rs no longer quotes its diagnostics; \
printf's prefix match cannot work"
);
for name in named {
let style = match name {
"C_NO_QUOTES" => QuotingStyle::C_NO_QUOTES,
"Escape" => QuotingStyle::Escape,
"SHELL_ESCAPE" => QuotingStyle::SHELL_ESCAPE,
other => panic!("generated code names unknown QuotingStyle::{other}"),
};
assert_eq!(
locale_aware_escape_name(probe, style),
expected,
"generated code quotes with QuotingStyle::{name}, which escapes \
differently from DIAGNOSTIC_QUOTING_STYLE; update printf.rs"
);
}
}
}