Skip to content

Commit bd237d9

Browse files
committed
Implement FuriString::{chars_lossy, char_indices_lossy, bytes}
This enables us to improve the `Debug` and `Display` impls.
1 parent f96d544 commit bd237d9

File tree

3 files changed

+101
-24
lines changed

3 files changed

+101
-24
lines changed

crates/flipperzero/src/furi/string.rs

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ use alloc::{borrow::Cow, boxed::Box, ffi::CString};
1616

1717
use flipperzero_sys as sys;
1818

19+
mod iter;
20+
use self::iter::{Bytes, CharIndices, Chars};
21+
1922
mod pattern;
2023
use self::pattern::Pattern;
2124

@@ -243,6 +246,50 @@ impl FuriString {
243246
// and that are useful for a non-slice string. Some of these are altered to be mutating
244247
// as we can't provide string slices.
245248
impl FuriString {
249+
/// Returns an iterator over the [`char`]s of a `FuriString`.
250+
///
251+
/// A `FuriString` might not contain valid UTF-8 (for example, if it represents a
252+
/// string obtained through the Flipper Zero SDK).Any invalid UTF-8 sequences will be
253+
/// replaced with [`U+FFFD REPLACEMENT CHARACTER`][U+FFFD], which looks like this: �
254+
///
255+
/// [U+FFFD]: core::char::REPLACEMENT_CHARACTER
256+
///
257+
/// It's important to remember that [`char`] represents a Unicode Scalar
258+
/// Value, and might not match your idea of what a 'character' is. Iteration
259+
/// over grapheme clusters may be what you actually want.
260+
#[inline]
261+
pub fn chars_lossy(&self) -> Chars<'_> {
262+
Chars {
263+
iter: self.to_bytes().iter(),
264+
}
265+
}
266+
267+
/// Returns an iterator over the [`char`]s of a `FuriString`, and their positions.
268+
///
269+
/// A `FuriString` might not contain valid UTF-8 (for example, if it represents a
270+
/// string obtained through the Flipper Zero SDK).Any invalid UTF-8 sequences will be
271+
/// replaced with [`U+FFFD REPLACEMENT CHARACTER`][U+FFFD], which looks like this: �
272+
///
273+
/// [U+FFFD]: core::char::REPLACEMENT_CHARACTER
274+
///
275+
/// The iterator yields tuples. The position is first, the [`char`] is second.
276+
#[inline]
277+
pub fn char_indices_lossy(&self) -> CharIndices<'_> {
278+
CharIndices {
279+
front_offset: 0,
280+
iter: self.chars_lossy(),
281+
}
282+
}
283+
284+
/// An iterator over the bytes of a string slice.
285+
///
286+
/// As a string consists of a sequence of bytes, we can iterate through a string by
287+
/// byte. This method returns such an iterator.
288+
#[inline]
289+
pub fn bytes(&self) -> Bytes<'_> {
290+
Bytes(self.to_bytes().iter().copied())
291+
}
292+
246293
/// Returns `true` if the given pattern matches a sub-slice of this string slice.
247294
///
248295
/// Returns `false` if it does not.
@@ -716,8 +763,8 @@ impl fmt::Debug for FuriString {
716763
#[inline]
717764
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
718765
f.write_char('"')?;
719-
for byte in self.to_bytes().escape_ascii() {
720-
f.write_char(byte as char)?;
766+
for c in self.chars_lossy() {
767+
f.write_char(c)?;
721768
}
722769
f.write_char('"')
723770
}
@@ -730,8 +777,8 @@ impl ufmt::uDebug for FuriString {
730777
W: ufmt::uWrite + ?Sized,
731778
{
732779
f.write_char('"')?;
733-
for byte in self.to_bytes().escape_ascii() {
734-
f.write_char(byte as char)?;
780+
for c in self.chars_lossy() {
781+
f.write_char(c)?;
735782
}
736783
f.write_char('"')
737784
}
@@ -740,8 +787,8 @@ impl ufmt::uDebug for FuriString {
740787
impl fmt::Display for FuriString {
741788
#[inline]
742789
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
743-
for byte in self.to_bytes().escape_ascii() {
744-
f.write_char(byte as char)?;
790+
for c in self.chars_lossy() {
791+
f.write_char(c)?;
745792
}
746793
Ok(())
747794
}
@@ -753,8 +800,8 @@ impl ufmt::uDisplay for FuriString {
753800
where
754801
W: ufmt::uWrite + ?Sized,
755802
{
756-
for byte in self.to_bytes().escape_ascii() {
757-
f.write_char(byte as char)?;
803+
for c in self.chars_lossy() {
804+
f.write_char(c)?;
758805
}
759806
Ok(())
760807
}
@@ -789,3 +836,26 @@ impl ufmt::uWrite for FuriString {
789836
Ok(())
790837
}
791838
}
839+
840+
#[flipperzero_test::tests]
841+
mod tests {
842+
use flipperzero_sys as sys;
843+
844+
use super::FuriString;
845+
846+
#[test]
847+
fn invalid_utf8_is_replaced() {
848+
// The German word für encoded in ISO 8859-1.
849+
let d: [u8; 3] = [0x66, 0xfc, 0x72];
850+
851+
// Construct an invalid string using the Flipper Zero SDK.
852+
let s = FuriString::new();
853+
for b in d {
854+
unsafe { sys::furi_string_push_back(s.0, b as i8) };
855+
}
856+
857+
for (l, r) in s.chars_lossy().zip("f�r".chars()) {
858+
assert_eq!(l, r);
859+
}
860+
}
861+
}

crates/flipperzero/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ flipperzero_test::tests_runner!(
2626
[
2727
crate::furi::message_queue::tests,
2828
crate::furi::rng::tests,
29+
crate::furi::string::tests,
2930
crate::furi::sync::tests,
3031
crate::toolbox::crc32::tests,
3132
crate::toolbox::md5::tests,

crates/flipperzero/tests/string.rs

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -456,8 +456,8 @@ mod tests {
456456
let t = "ศไทย中华";
457457
let u = "Việt Nam";
458458

459-
// let a: String = s.chars().collect();
460-
// assert_eq!(s, a);
459+
let a: String = s.chars_lossy().collect();
460+
assert_eq!(s, a);
461461

462462
let mut b = String::from(t);
463463
b.extend(u.chars());
@@ -850,17 +850,17 @@ mod str_tests {
850850
// );
851851
}
852852

853-
/*
854853
#[test]
855854
fn collect() {
856855
let empty = String::from("");
857-
let s: String = empty.chars().collect();
856+
let s: String = empty.chars_lossy().collect();
858857
assert_eq!(empty, s);
859858
let data = String::from("ประเทศไทย中");
860-
let s: String = data.chars().collect();
859+
let s: String = data.chars_lossy().collect();
861860
assert_eq!(data, s);
862861
}
863862

863+
/*
864864
#[test]
865865
fn into_bytes() {
866866
let data = String::from("asdf");
@@ -1929,7 +1929,6 @@ mod str_tests {
19291929
assert_eq!(String::from("22").cmp(&String::from("1234")), Greater);
19301930
}
19311931

1932-
/*
19331932
#[test]
19341933
fn iterator() {
19351934
let s = String::from("ศไทย中华Việt Nam");
@@ -1938,16 +1937,17 @@ mod str_tests {
19381937
];
19391938

19401939
let mut pos = 0;
1941-
let it = s.chars();
1940+
let it = s.chars_lossy();
19421941

19431942
for c in it {
19441943
assert_eq!(c, v[pos]);
19451944
pos += 1;
19461945
}
19471946
assert_eq!(pos, v.len());
1948-
assert_eq!(s.chars().count(), v.len());
1947+
assert_eq!(s.chars_lossy().count(), v.len());
19491948
}
19501949

1950+
/*
19511951
#[test]
19521952
fn rev_iterator() {
19531953
let s = String::from("ศไทย中华Việt Nam");
@@ -2004,8 +2004,8 @@ mod str_tests {
20042004
fn chars_decoding() {
20052005
let mut bytes = [0; 4];
20062006
for c in (0..0x110000).filter_map(core::char::from_u32) {
2007-
let s = c.encode_utf8(&mut bytes);
2008-
if Some(c) != s.chars().next() {
2007+
let s = String::from(c.encode_utf8(&mut bytes));
2008+
if Some(c) != s.chars_lossy().next() {
20092009
panic!("character {:x}={} does not decode correctly", c as u32, c);
20102010
}
20112011
}
@@ -2021,32 +2021,35 @@ mod str_tests {
20212021
}
20222022
}
20232023
}
2024+
*/
20242025

20252026
#[test]
20262027
fn iterator_clone() {
20272028
let s = String::from("ศไทย中华Việt Nam");
2028-
let mut it = s.chars();
2029+
let mut it = s.chars_lossy();
20292030
it.next();
20302031
assert!(it.clone().zip(it).all(|(x, y)| x == y));
20312032
}
20322033

20332034
#[test]
20342035
fn iterator_last() {
20352036
let s = String::from("ศไทย中华Việt Nam");
2036-
let mut it = s.chars();
2037+
let mut it = s.chars_lossy();
20372038
it.next();
20382039
assert_eq!(it.last(), Some('m'));
20392040
}
20402041

2042+
/*
20412043
#[test]
20422044
fn chars_debug() {
2043-
let s = "ศไทย中华Việt Nam";
2044-
let c = s.chars();
2045+
let s = String::from("ศไทย中华Việt Nam");
2046+
let c = s.chars_lossy();
20452047
assert_eq!(
20462048
format!("{c:?}"),
20472049
r#"Chars(['ศ', 'ไ', 'ท', 'ย', '中', '华', 'V', 'i', 'ệ', 't', ' ', 'N', 'a', 'm'])"#
20482050
);
20492051
}
2052+
*/
20502053

20512054
#[test]
20522055
fn bytesator() {
@@ -2117,7 +2120,7 @@ mod str_tests {
21172120
];
21182121

21192122
let mut pos = 0;
2120-
let it = s.char_indices();
2123+
let it = s.char_indices_lossy();
21212124

21222125
for c in it {
21232126
assert_eq!(c, (p[pos], v[pos]));
@@ -2127,6 +2130,7 @@ mod str_tests {
21272130
assert_eq!(pos, p.len());
21282131
}
21292132

2133+
/*
21302134
#[test]
21312135
fn char_indices_revator() {
21322136
let s = String::from("ศไทย中华Việt Nam");
@@ -2145,15 +2149,17 @@ mod str_tests {
21452149
assert_eq!(pos, v.len());
21462150
assert_eq!(pos, p.len());
21472151
}
2152+
*/
21482153

21492154
#[test]
21502155
fn char_indices_last() {
21512156
let s = String::from("ศไทย中华Việt Nam");
2152-
let mut it = s.char_indices();
2157+
let mut it = s.char_indices_lossy();
21532158
it.next();
21542159
assert_eq!(it.last(), Some((27, 'm')));
21552160
}
21562161

2162+
/*
21572163
#[test]
21582164
fn splitn_char_iterator() {
21592165
let data = "\nMäry häd ä little lämb\nLittle lämb\n";

0 commit comments

Comments
 (0)