Skip to main content

nsis_plugin/
tchar.rs

1//! The character-width abstraction.
2//!
3//! C solves this with `TCHAR` and two compilations. We do the same, but behind
4//! a type: `Tchar` is `u16` in a Unicode build and `u8` in an ANSI build, and
5//! the public API traffics in `String`/`&str`, converting at the boundary.
6//!
7//! The conversion mirrors `Contrib/ExDLL/pluginapi.c`: UTF-16 in the Unicode
8//! build, `CP_ACP` via `MultiByteToWideChar`/`WideCharToMultiByte` in the ANSI
9//! build.
10
11#![allow(
12	clippy::undocumented_unsafe_blocks,
13	reason = "each function documents the buffer it requires under `# Safety`; \
14	          the Win32 conversion calls are bounded by lengths computed by \
15	          the preceding sizing call"
16)]
17
18use alloc::string::String;
19use alloc::vec::Vec;
20
21#[cfg(all(feature = "ansi", feature = "unicode"))]
22compile_error!(
23	"the `ansi` and `unicode` features of nsis-plugin are mutually exclusive; \
24	 build an ANSI plug-in with `default-features = false, features = [\"ansi\"]`"
25);
26
27#[cfg(not(any(feature = "ansi", feature = "unicode")))]
28compile_error!("nsis-plugin needs exactly one of the `ansi` or `unicode` features");
29
30/// The installer's character type: `u16` under `unicode`, `u8` under `ansi`.
31#[cfg(feature = "unicode")]
32pub type Tchar = u16;
33
34/// The installer's character type: `u16` under `unicode`, `u8` under `ansi`.
35#[cfg(all(feature = "ansi", not(feature = "unicode")))]
36pub type Tchar = u8;
37
38/// Reads a NUL-terminated string, never looking past `max` characters.
39///
40/// `pluginapi.c` reads with an unbounded `lstrcpy`; we bound every read at the
41/// installer's `string_size`, which is the whole point of this crate.
42///
43/// # Safety
44/// `ptr` must be readable for `max` elements.
45pub unsafe fn read_bounded(ptr: *const Tchar, max: usize) -> String {
46	let len = unsafe { strlen_bounded(ptr, max) };
47	let units = unsafe { core::slice::from_raw_parts(ptr, len) };
48	decode(units)
49}
50
51/// Length of the NUL-terminated string at `ptr`, capped at `max`.
52///
53/// # Safety
54/// `ptr` must be readable for `max` elements.
55pub unsafe fn strlen_bounded(ptr: *const Tchar, max: usize) -> usize {
56	let mut len = 0;
57	while len < max && unsafe { *ptr.add(len) } != 0 {
58		len += 1;
59	}
60	len
61}
62
63/// Copies `src` into `dst` with `lstrcpyn(dst, src, capacity)` semantics:
64/// at most `capacity - 1` characters plus a NUL terminator.
65///
66/// Returns `true` when the whole source fit.
67///
68/// # Safety
69/// `dst` must be writable for `capacity` elements.
70pub unsafe fn write_bounded(dst: *mut Tchar, capacity: usize, src: &[Tchar]) -> bool {
71	if capacity == 0 {
72		return src.is_empty();
73	}
74	let room = capacity - 1;
75	let n = src.len().min(room);
76	unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), dst, n) };
77	unsafe { *dst.add(n) = 0 };
78	src.len() <= room
79}
80
81// -- Unicode ----------------------------------------------------------------
82
83/// Encodes a `&str` as installer characters, without a NUL terminator.
84#[cfg(feature = "unicode")]
85pub fn encode(s: &str) -> Vec<Tchar> {
86	s.encode_utf16().collect()
87}
88
89/// Decodes installer characters into a `String`, replacing anything invalid.
90#[cfg(feature = "unicode")]
91pub fn decode(units: &[Tchar]) -> String {
92	decode_utf16(units)
93}
94
95/// UTF-16 to `String`, replacing unpaired surrogates rather than failing.
96#[cfg(any(feature = "unicode", target_os = "windows"))]
97fn decode_utf16(units: &[u16]) -> String {
98	char::decode_utf16(units.iter().copied())
99		.map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER))
100		.collect()
101}
102
103// -- ANSI -------------------------------------------------------------------
104
105#[cfg(all(feature = "ansi", not(feature = "unicode"), target_os = "windows"))]
106mod ansi {
107	use alloc::string::String;
108	use alloc::vec;
109	use alloc::vec::Vec;
110	use core::ffi::{c_char, c_int, c_void};
111
112	const CP_ACP: u32 = 0;
113
114	#[link(name = "kernel32")]
115	unsafe extern "system" {
116		fn MultiByteToWideChar(
117			codepage: u32,
118			flags: u32,
119			mbstr: *const c_char,
120			cbmb: c_int,
121			wcstr: *mut u16,
122			cchwc: c_int,
123		) -> c_int;
124		fn WideCharToMultiByte(
125			codepage: u32,
126			flags: u32,
127			wcstr: *const u16,
128			cchwc: c_int,
129			mbstr: *mut c_char,
130			cbmb: c_int,
131			default_char: *const c_char,
132			used_default: *mut c_void,
133		) -> c_int;
134	}
135
136	pub fn encode(s: &str) -> Vec<u8> {
137		let wide: Vec<u16> = s.encode_utf16().collect();
138		if wide.is_empty() {
139			return Vec::new();
140		}
141		let len = wide.len() as c_int;
142		// Explicit length, so no NUL terminator is appended.
143		let needed = unsafe {
144			WideCharToMultiByte(
145				CP_ACP,
146				0,
147				wide.as_ptr(),
148				len,
149				core::ptr::null_mut(),
150				0,
151				core::ptr::null(),
152				core::ptr::null_mut(),
153			)
154		};
155		if needed <= 0 {
156			return Vec::new();
157		}
158		let mut buf = vec![0u8; needed as usize];
159		unsafe {
160			WideCharToMultiByte(
161				CP_ACP,
162				0,
163				wide.as_ptr(),
164				len,
165				buf.as_mut_ptr().cast(),
166				needed,
167				core::ptr::null(),
168				core::ptr::null_mut(),
169			)
170		};
171		buf
172	}
173
174	pub fn decode(units: &[u8]) -> String {
175		if units.is_empty() {
176			return String::new();
177		}
178		let len = units.len() as c_int;
179		let needed = unsafe {
180			MultiByteToWideChar(
181				CP_ACP,
182				0,
183				units.as_ptr().cast(),
184				len,
185				core::ptr::null_mut(),
186				0,
187			)
188		};
189		if needed <= 0 {
190			return String::new();
191		}
192		let mut wide = vec![0u16; needed as usize];
193		unsafe {
194			MultiByteToWideChar(
195				CP_ACP,
196				0,
197				units.as_ptr().cast(),
198				len,
199				wide.as_mut_ptr(),
200				needed,
201			)
202		};
203		super::decode_utf16(&wide)
204	}
205}
206
207/// Host stand-in for the `CP_ACP` codepage, so ANSI logic is testable off
208/// Windows. Latin-1 is not `CP_ACP`, and this path is never compiled into a
209/// shipped plug-in — it exists only for `cargo test` on macOS and Linux.
210#[cfg(all(feature = "ansi", not(feature = "unicode"), not(target_os = "windows")))]
211mod ansi {
212	use alloc::string::String;
213	use alloc::vec::Vec;
214
215	pub fn encode(s: &str) -> Vec<u8> {
216		s.chars()
217			.map(|c| if (c as u32) < 0x100 { c as u8 } else { b'?' })
218			.collect()
219	}
220
221	pub fn decode(units: &[u8]) -> String {
222		units.iter().map(|&b| b as char).collect()
223	}
224}
225
226/// Encodes a `&str` as installer characters, without a NUL terminator.
227#[cfg(all(feature = "ansi", not(feature = "unicode")))]
228pub fn encode(s: &str) -> Vec<Tchar> {
229	ansi::encode(s)
230}
231
232/// Decodes installer characters into a `String`, replacing anything invalid.
233#[cfg(all(feature = "ansi", not(feature = "unicode")))]
234pub fn decode(units: &[Tchar]) -> String {
235	ansi::decode(units)
236}