Skip to content

Commit 9ff9d25

Browse files
committed
Fix newline issues on Windows.
Now all text is checked on input and output for the correct line endings, normalized to UNIX (CR) line endings.
1 parent 6717cbc commit 9ff9d25

File tree

6 files changed

+112
-40
lines changed

6 files changed

+112
-40
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
1313
- `RadioButtons` control for groups of radio buttons
1414
- `Combobox::selected()` method to retrieve the currently selected index of the combobox
1515
- Officially move communications to the Matrix room #rust-native-ui:matrix.nora.codes
16+
* `str_tools` module provides utilities for converting to and from system `CString` and
17+
`CStr` values, while enforcing correct newline values (CR vs CRLF).
1618

1719
### Changed
1820

@@ -29,12 +31,15 @@ No deprecations.
2931
### Removed
3032

3133
* `Transform` no longer implements `PartialEq` as the existing implementation was broken.
34+
* `Button` and `Label` no longer implement `text_ref` as we cannot ensure toolkit newline
35+
compliance.
3236

3337
### Fixed
3438

3539
* `VerticalBox` and `HorizontalBox` no longer link to the removed `BoxExt` trait.
3640
* `ui-sys` now builds on modern macOS.
3741
* `inputs` and `inputs-grid` examples no longer erroneously start at 0 for inputs starting at 1.
42+
* Text no longer uses incorrect newlines per platform.
3843

3944
### Security
4045

iui/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ bitflags = "1.0"
4242
libc = "0.2"
4343
failure = "0.1.1"
4444
ui-sys = { path = "../ui-sys", version = "0.2.1" }
45+
regex = "1"
46+
lazy_static = "1"
47+

iui/src/controls/basic.rs

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use super::Control;
22
use std::os::raw::c_void;
3-
use std::ffi::{CStr, CString};
43
use std::mem;
54
use ui::UI;
65
use ui_sys::{self, uiButton, uiControl, uiLabel};
6+
use str_tools::{from_toolkit_string, to_toolkit_string};
77

88
define_control!{
99
/// A non-interactable piece of text.
@@ -20,30 +20,23 @@ define_control!{
2020
impl Button {
2121
/// Create a new button with the given text as its label.
2222
pub fn new(_ctx: &UI, text: &str) -> Button {
23+
let c_string = to_toolkit_string(text);
2324
unsafe {
24-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
2525
Button::from_raw(ui_sys::uiNewButton(c_string.as_ptr()))
2626
}
2727
}
2828

2929
/// Get a copy of the existing text on the button.
3030
pub fn text(&self, _ctx: &UI) -> String {
3131
unsafe {
32-
CStr::from_ptr(ui_sys::uiButtonText(self.uiButton))
33-
.to_string_lossy()
34-
.into_owned()
32+
from_toolkit_string(ui_sys::uiButtonText(self.uiButton))
3533
}
3634
}
3735

38-
/// Get a reference to the existing text on the button.
39-
pub fn text_ref(&self, _ctx: &UI) -> &CStr {
40-
unsafe { CStr::from_ptr(ui_sys::uiButtonText(self.uiButton)) }
41-
}
42-
4336
/// Set the text on the button.
4437
pub fn set_text(&mut self, _ctx: &UI, text: &str) {
38+
let c_string = to_toolkit_string(text);
4539
unsafe {
46-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
4740
ui_sys::uiButtonSetText(self.uiButton, c_string.as_ptr())
4841
}
4942
}
@@ -74,30 +67,23 @@ impl Label {
7467
/// Note that labels do not auto-wrap their text; they will expand as far as needed
7568
/// to fit.
7669
pub fn new(_ctx: &UI, text: &str) -> Label {
70+
let c_string = to_toolkit_string(text);
7771
unsafe {
78-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
7972
Label::from_raw(ui_sys::uiNewLabel(c_string.as_ptr()))
8073
}
8174
}
8275

8376
/// Get a copy of the existing text on the label.
8477
pub fn text(&self, _ctx: &UI) -> String {
8578
unsafe {
86-
CStr::from_ptr(ui_sys::uiLabelText(self.uiLabel))
87-
.to_string_lossy()
88-
.into_owned()
79+
from_toolkit_string(ui_sys::uiLabelText(self.uiLabel))
8980
}
9081
}
9182

92-
/// Get a reference to the existing text on the label.
93-
pub fn text_ref(&self, _ctx: &UI) -> &CStr {
94-
unsafe { CStr::from_ptr(ui_sys::uiLabelText(self.uiLabel)) }
95-
}
96-
9783
/// Set the text on the label.
9884
pub fn set_text(&mut self, _ctx: &UI, text: &str) {
85+
let c_string = to_toolkit_string(text);
9986
unsafe {
100-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
10187
ui_sys::uiLabelSetText(self.uiLabel, c_string.as_ptr())
10288
}
10389
}

iui/src/controls/entry.rs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
//! User input mechanisms: numbers, colors, and text in various forms.
2+
//!
3+
//! All text buffers accept and return `\n` line endings; if on Windows, the appropriate
4+
//! `\r\n` for display are added and removed by the controls.
25
36
use super::Control;
47
use std::ffi::{CStr, CString};
58
use std::i32;
69
use std::mem;
710
use std::os::raw::c_void;
8-
use ui::UI;
911
use ui_sys::{
1012
self, uiCheckbox, uiCombobox, uiControl, uiEntry, uiMultilineEntry, uiRadioButtons, uiSlider,
1113
uiSpinbox,
1214
};
15+
use ui::UI;
16+
use str_tools::{from_toolkit_string, to_toolkit_string};
1317

1418
pub trait NumericEntry {
1519
fn value(&self, ctx: &UI) -> i32;
@@ -150,14 +154,11 @@ impl MultilineEntry {
150154

151155
impl TextEntry for Entry {
152156
fn value(&self, _ctx: &UI) -> String {
153-
unsafe {
154-
CStr::from_ptr(ui_sys::uiEntryText(self.uiEntry))
155-
.to_string_lossy()
156-
.into_owned()
157-
}
157+
unsafe { from_toolkit_string(ui_sys::uiEntryText(self.uiEntry)) }
158158
}
159+
159160
fn set_value(&mut self, _ctx: &UI, value: &str) {
160-
let cstring = CString::new(value.as_bytes().to_vec()).unwrap();
161+
let cstring = to_toolkit_string(value);
161162
unsafe { ui_sys::uiEntrySetText(self.uiEntry, cstring.as_ptr()) }
162163
}
163164

@@ -210,9 +211,7 @@ impl TextEntry for PasswordEntry {
210211

211212
extern "C" fn c_callback(entry: *mut uiEntry, data: *mut c_void) {
212213
unsafe {
213-
let string = CStr::from_ptr(ui_sys::uiEntryText(entry))
214-
.to_string_lossy()
215-
.into_owned();
214+
let string = from_toolkit_string(ui_sys::uiEntryText(entry));
216215
mem::transmute::<*mut c_void, &mut Box<dyn FnMut(String)>>(data)(string);
217216
mem::forget(entry);
218217
}
@@ -223,13 +222,12 @@ impl TextEntry for PasswordEntry {
223222
impl TextEntry for MultilineEntry {
224223
fn value(&self, _ctx: &UI) -> String {
225224
unsafe {
226-
CStr::from_ptr(ui_sys::uiMultilineEntryText(self.uiMultilineEntry))
227-
.to_string_lossy()
228-
.into_owned()
225+
from_toolkit_string(ui_sys::uiMultilineEntryText(self.uiMultilineEntry))
229226
}
230227
}
228+
231229
fn set_value(&mut self, _ctx: &UI, value: &str) {
232-
let cstring = CString::new(value.as_bytes().to_vec()).unwrap();
230+
let cstring = to_toolkit_string(value);
233231
unsafe { ui_sys::uiMultilineEntrySetText(self.uiMultilineEntry, cstring.as_ptr()) }
234232
}
235233

@@ -246,9 +244,7 @@ impl TextEntry for MultilineEntry {
246244

247245
extern "C" fn c_callback(entry: *mut uiMultilineEntry, data: *mut c_void) {
248246
unsafe {
249-
let string = CStr::from_ptr(ui_sys::uiMultilineEntryText(entry))
250-
.to_string_lossy()
251-
.into_owned();
247+
let string = from_toolkit_string(ui_sys::uiMultilineEntryText(entry));
252248
mem::transmute::<*mut c_void, &mut Box<dyn FnMut(String)>>(data)(string);
253249
mem::forget(entry);
254250
}
@@ -271,7 +267,7 @@ impl Combobox {
271267
/// Adds a new option to the combination box.
272268
pub fn append(&self, _ctx: &UI, name: &str) {
273269
unsafe {
274-
let c_string = CString::new(name.as_bytes().to_vec()).unwrap();
270+
let c_string = to_toolkit_string(name);
275271
ui_sys::uiComboboxAppend(self.uiCombobox, c_string.as_ptr())
276272
}
277273
}
@@ -314,7 +310,7 @@ define_control! {
314310
impl Checkbox {
315311
// Create a new Checkbox which can produce values from `min` to `max`.
316312
pub fn new(_ctx: &UI, text: &str) -> Self {
317-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
313+
let c_string = to_toolkit_string(text);
318314
unsafe { Checkbox::from_raw(ui_sys::uiNewCheckbox(c_string.as_ptr())) }
319315
}
320316

iui/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@
2121
extern crate bitflags;
2222
#[macro_use]
2323
extern crate failure;
24+
#[macro_use]
25+
extern crate lazy_static;
2426
extern crate libc;
2527
extern crate ui_sys;
28+
extern crate regex;
2629

2730
pub mod controls;
2831
pub mod draw;
2932
mod error;
3033
mod ffi_tools;
3134
pub mod menus;
3235
mod ui;
36+
pub mod str_tools;
3337

3438
pub use error::UIError;
3539
pub use ui::{EventLoop, UI};

iui/src/str_tools.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Tools for making platform-independent string handling work properly
2+
3+
use regex::{Regex, RegexBuilder};
4+
use std::ffi::{CStr, CString};
5+
use std::os::raw::c_char;
6+
7+
/// Replaces every occurrance of `"\r\n"` with a single newline `\n`, without collapsing
8+
/// newlines.
9+
pub fn strip_dual_endings(s: &str) -> String {
10+
s.replace("\r\n", "\n")
11+
}
12+
13+
/// Replaces every occurrance of `"\n"` not followed by `"\r"` with `"\r\n"`.
14+
pub fn insert_dual_endings(s: &str) -> String {
15+
lazy_static! {
16+
//static ref RE: Regex = Regex::new("([^\r])\n").expect("Could not compile regex");
17+
static ref RE: Regex = RegexBuilder::new("\r\n|\n")
18+
.multi_line(true)
19+
.build()
20+
.expect("Could not compile regex");
21+
}
22+
RE.replace_all(s, "\r\n").to_string()
23+
}
24+
25+
/// Converts a &str to a CString, using either LF or CRLF as appropriate.
26+
///
27+
/// # Panics
28+
/// Panics if it isn't possible to create a CString from the given string.
29+
pub fn to_toolkit_string(s: &str) -> CString {
30+
let data = if cfg!(windows) {
31+
insert_dual_endings(s).as_bytes().to_vec()
32+
} else {
33+
s.as_bytes().to_vec()
34+
};
35+
CString::new(data).expect(&format!("Failed to create CString from {}", s))
36+
}
37+
38+
/// Converts a `*mut c_char` to a String guaranteed to use LF line endings.
39+
///
40+
/// # Unsafety
41+
/// Has the same unsafety as [CStr::from_ptr](https://doc.rust-lang.org/std/ffi/struct.CStr.html#method.from_ptr).
42+
pub unsafe fn from_toolkit_string(c: *mut c_char) -> String {
43+
CStr::from_ptr(c).to_string_lossy().into_owned()
44+
}
45+
46+
#[cfg(test)]
47+
mod test {
48+
use super::*;
49+
50+
#[test]
51+
fn strip_dual_endings_to_single() {
52+
assert_eq!(strip_dual_endings("Line 1\r\nLine 2\r\n"),
53+
"Line 1\nLine 2\n");
54+
}
55+
56+
#[test]
57+
fn insert_dual_endings_basic() {
58+
assert_eq!(insert_dual_endings("Line 1\nLine 2\n"),
59+
"Line 1\r\nLine 2\r\n");
60+
}
61+
62+
#[test]
63+
fn insert_dual_endings_nodupe() {
64+
assert_eq!(insert_dual_endings("Line 1\r\nLine 2\r\n"),
65+
"Line 1\r\nLine 2\r\n");
66+
}
67+
68+
#[test]
69+
fn test_toolkit_roundtripping() {
70+
let initial_string = "Here is some test data.\n\nMultiline!\n";
71+
let toolkit_string = to_toolkit_string(initial_string);
72+
let roundtripped_string = unsafe {
73+
from_toolkit_string(toolkit_string.into_raw())
74+
};
75+
assert_eq!(initial_string, &roundtripped_string);
76+
}
77+
}
78+

0 commit comments

Comments
 (0)