Skip to content

Commit cef81dc

Browse files
Fix handling of trailing bare CR in str::lines
Previously "bare\r" was split into ["bare"] even though the documentation said that only LF and CRLF count as newlines. This fix is a behavioural change, even though it brings the behaviour into line with the documentation, and into line with that of `std::io::BufRead::lines()`. This is an alternative to #91051, which proposes to document rather than fix the behaviour. Fixes #94435. Co-authored-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
1 parent 0d8a0c5 commit cef81dc

File tree

3 files changed

+24
-12
lines changed

3 files changed

+24
-12
lines changed

library/alloc/tests/str.rs

+19-7
Original file line numberDiff line numberDiff line change
@@ -1499,13 +1499,25 @@ fn test_split_whitespace() {
14991499

15001500
#[test]
15011501
fn test_lines() {
1502-
let data = "\nMäry häd ä little lämb\n\r\nLittle lämb\n";
1503-
let lines: Vec<&str> = data.lines().collect();
1504-
assert_eq!(lines, ["", "Märy häd ä little lämb", "", "Little lämb"]);
1505-
1506-
let data = "\r\nMäry häd ä little lämb\n\nLittle lämb"; // no trailing \n
1507-
let lines: Vec<&str> = data.lines().collect();
1508-
assert_eq!(lines, ["", "Märy häd ä little lämb", "", "Little lämb"]);
1502+
fn t(data: &str, expected: &[&str]) {
1503+
let lines: Vec<&str> = data.lines().collect();
1504+
assert_eq!(lines, expected);
1505+
}
1506+
t("", &[]);
1507+
t("\n", &[""]);
1508+
t("\n2nd", &["", "2nd"]);
1509+
t("\r\n", &[""]);
1510+
t("bare\r", &["bare\r"]);
1511+
t("bare\rcr", &["bare\rcr"]);
1512+
t("Text\n\r", &["Text", "\r"]);
1513+
t(
1514+
"\nMäry häd ä little lämb\n\r\nLittle lämb\n",
1515+
&["", "Märy häd ä little lämb", "", "Little lämb"],
1516+
);
1517+
t(
1518+
"\r\nMäry häd ä little lämb\n\nLittle lämb",
1519+
&["", "Märy häd ä little lämb", "", "Little lämb"],
1520+
);
15091521
}
15101522

15111523
#[test]

library/core/src/str/iter.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@ generate_pattern_iterators! {
10911091
#[stable(feature = "rust1", since = "1.0.0")]
10921092
#[must_use = "iterators are lazy and do nothing unless consumed"]
10931093
#[derive(Clone, Debug)]
1094-
pub struct Lines<'a>(pub(super) Map<SplitTerminator<'a, char>, LinesMap>);
1094+
pub struct Lines<'a>(pub(super) Map<SplitInclusive<'a, char>, LinesMap>);
10951095

10961096
#[stable(feature = "rust1", since = "1.0.0")]
10971097
impl<'a> Iterator for Lines<'a> {

library/core/src/str/mod.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,7 @@ impl str {
997997
#[stable(feature = "rust1", since = "1.0.0")]
998998
#[inline]
999999
pub fn lines(&self) -> Lines<'_> {
1000-
Lines(self.split_terminator('\n').map(LinesMap))
1000+
Lines(self.split_inclusive('\n').map(LinesMap))
10011001
}
10021002

10031003
/// An iterator over the lines of a string.
@@ -2591,9 +2591,9 @@ impl_fn_for_zst! {
25912591
/// A nameable, cloneable fn type
25922592
#[derive(Clone)]
25932593
struct LinesMap impl<'a> Fn = |line: &'a str| -> &'a str {
2594-
let l = line.len();
2595-
if l > 0 && line.as_bytes()[l - 1] == b'\r' { &line[0 .. l - 1] }
2596-
else { line }
2594+
let Some(line) = line.strip_suffix('\n') else { return line };
2595+
let Some(line) = line.strip_suffix('\r') else { return line };
2596+
line
25972597
};
25982598

25992599
#[derive(Clone)]

0 commit comments

Comments
 (0)