1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! One C/POSIX-locale codec on every platform.
//!
//! zsh hands every multibyte decision to libc's `mbrtowc`/`wcrtomb`, and
//! the platforms disagree about the C locale, whose codeset is ASCII
//! (`nl_langinfo(CODESET)` is `ANSI_X3.4-1968` on glibc and `US-ASCII` on
//! macOS):
//!
//! * glibc rejects every byte above 0x7f with `EILSEQ`, so zsh takes its
//! `MB_INVALID` arms and prints such a byte as `\M-c`
//! (`c:Src/utils.c:5396-5403`);
//! * macOS returns 1 and hands back the byte value as the wide character,
//! so the same zsh prints 0xe3 raw.
//!
//! zsh's own test suite expects the glibc output (`E02xtrace.ztst`:
//! `LC_ALL=C which ヌ` prints `$'\M-c\M-\C-C\M-\C-L'`), and zshrs has to
//! print the same thing on every platform. So under an ASCII codeset these
//! wrappers reject what ASCII cannot hold: a byte above 0x7f does not decode
//! and a wide character above 0x7f does not encode. Every other locale
//! (UTF-8, ISO-8859-*, …) goes to libc untouched.
//!
//! `crate::ported::utils` re-exports both functions under the libc names,
//! so every ported call site gets the same answer without restating it.
use std::ffi::CStr;
extern "C" {
#[link_name = "mbrtowc"]
fn libc_mbrtowc(
pwc: *mut libc::wchar_t,
s: *const libc::c_char,
n: libc::size_t,
ps: *mut libc::c_void,
) -> libc::size_t;
#[link_name = "wcrtomb"]
fn libc_wcrtomb(s: *mut libc::c_char, wc: libc::wchar_t, ps: *mut libc::c_void)
-> libc::size_t;
}
/// `(size_t)-1` — libc's "invalid sequence" result (zsh's `MB_INVALID`).
const MB_INVALID: libc::size_t = libc::size_t::MAX;
/// libc sets `errno = EILSEQ` alongside `(size_t)-1`; do the same.
fn set_eilseq() {
// SAFETY: the errno accessors return this thread's errno slot.
#[cfg(target_vendor = "apple")]
unsafe {
*libc::__error() = libc::EILSEQ;
}
#[cfg(target_os = "linux")]
unsafe {
*libc::__errno_location() = libc::EILSEQ;
}
}
/// True when the current `LC_CTYPE` codeset is ASCII — the C/POSIX locale.
/// Asked per call, because `LC_ALL=C cmd` and `LC_CTYPE=…` assignments
/// change the locale at run time.
fn codeset_is_ascii() -> bool {
// SAFETY: nl_langinfo returns a pointer to a NUL-terminated static
// string (or NULL), valid until the next setlocale on this thread.
let cs = unsafe { libc::nl_langinfo(libc::CODESET) };
if cs.is_null() {
return false;
}
let cs = unsafe { CStr::from_ptr(cs) }.to_bytes();
cs.eq_ignore_ascii_case(b"ANSI_X3.4-1968")
|| cs.eq_ignore_ascii_case(b"US-ASCII")
|| cs.eq_ignore_ascii_case(b"ASCII")
}
/// `mbrtowc(3)`, with a byte above 0x7f rejected under an ASCII codeset.
///
/// # Safety
/// Same contract as libc `mbrtowc`: `s` readable for `n` bytes (or NULL),
/// `ps` a valid `mbstate_t` buffer.
pub unsafe fn mbrtowc(
pwc: *mut libc::wchar_t,
s: *const libc::c_char,
n: libc::size_t,
ps: *mut libc::c_void,
) -> libc::size_t {
if !s.is_null() && n > 0 && *s as u8 > 0x7f && codeset_is_ascii() {
set_eilseq();
return MB_INVALID;
}
libc_mbrtowc(pwc, s, n, ps)
}
/// `wcrtomb(3)`, with a wide character above 0x7f rejected under an ASCII
/// codeset.
///
/// # Safety
/// Same contract as libc `wcrtomb`: `s` writable for `MB_CUR_MAX` bytes
/// (or NULL), `ps` a valid `mbstate_t` buffer.
pub unsafe fn wcrtomb(
s: *mut libc::c_char,
wc: libc::wchar_t,
ps: *mut libc::c_void,
) -> libc::size_t {
if !s.is_null() && wc as u32 > 0x7f && codeset_is_ascii() {
set_eilseq();
return MB_INVALID;
}
libc_wcrtomb(s, wc, ps)
}
/// libc `wint_t`: `unsigned int` on glibc/musl, `__int32_t` on Darwin.
#[cfg(target_vendor = "apple")]
type WintT = libc::c_int;
#[cfg(not(target_vendor = "apple"))]
type WintT = libc::c_uint;
extern "C" {
#[link_name = "iswalnum"]
fn libc_iswalnum(wc: WintT) -> libc::c_int;
}
/// `iswalnum(3)` for a non-ASCII character, the test zsh's `wcsitype` makes
/// (c:Src/utils.c:4350, 4353, 4370). Under an ASCII codeset no character above
/// 0x7f decodes, so none is alphanumeric: `LC_ALL=C; é=x` is a command word,
/// not an assignment. In every other locale the C library decides, and its
/// class is narrower than Unicode's: macOS rejects `Ⅻ`, `½`, `²` and `Ⓐ`.
pub fn iswalnum(c: char) -> bool {
if c as u32 > 0x7f && codeset_is_ascii() {
return false;
}
std::sync::LazyLock::force(&crate::ported::utils::MB_LOCALE_READY);
// SAFETY: iswalnum reads only its argument and the thread's locale.
unsafe { libc_iswalnum(c as WintT) != 0 }
}