Skip to content

Commit 8e65e79

Browse files
committed
format-flowed: make quotes round-trip
1 parent ecc7758 commit 8e65e79

File tree

4 files changed

+62
-25
lines changed

4 files changed

+62
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- Fix STARTTLS connection and add a test for it #3907
2727
- Trigger reconnection when failing to fetch existing messages #3911
2828
- Do not retry fetching existing messages after failure, prevents infinite reconnection loop #3913
29+
- Ensure format=flowed formatting is always reversible on the receiver side #3880
2930

3031

3132
## 1.104.0

format-flowed/src/lib.rs

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
fn format_line_flowed(line: &str, prefix: &str) -> String {
2525
let mut result = String::new();
2626
let mut buffer = prefix.to_string();
27-
let mut after_space = false;
27+
let mut after_space = prefix.ends_with(' ');
2828

2929
for c in line.chars() {
3030
if c == ' ' {
@@ -55,7 +55,7 @@ fn format_line_flowed(line: &str, prefix: &str) -> String {
5555
result + &buffer
5656
}
5757

58-
/// Returns text formatted according to RFC 3767 (format=flowed).
58+
/// Returns text formatted according to RFC 3676 (format=flowed).
5959
///
6060
/// This function accepts text separated by LF, but returns text
6161
/// separated by CRLF.
@@ -70,23 +70,20 @@ pub fn format_flowed(text: &str) -> String {
7070
result += "\r\n";
7171
}
7272

73-
let line_no_prefix = line
74-
.strip_prefix('>')
75-
.map(|line| line.strip_prefix(' ').unwrap_or(line));
76-
let is_quote = line_no_prefix.is_some();
77-
let line = line_no_prefix.unwrap_or(line).trim_end();
78-
let prefix = if is_quote { "> " } else { "" };
73+
let line = line.trim_end();
74+
let quote_depth = line.chars().take_while(|&c| c == '>').count();
75+
let (prefix, mut line) = line.split_at(quote_depth);
7976

80-
if prefix.len() + line.len() > 78 {
81-
result += &format_line_flowed(line, prefix);
82-
} else {
83-
result += prefix;
84-
if prefix.is_empty() && (line.starts_with('>') || line.starts_with(' ')) {
85-
// Space stuffing, see RFC 3676
86-
result.push(' ');
77+
let mut prefix = prefix.to_string();
78+
79+
if quote_depth > 0 {
80+
if let Some(s) = line.strip_prefix(' ') {
81+
line = s;
82+
prefix += " ";
8783
}
88-
result += line;
8984
}
85+
86+
result += &format_line_flowed(line, &prefix);
9087
}
9188

9289
result
@@ -111,16 +108,19 @@ pub fn format_flowed_quote(text: &str) -> String {
111108
///
112109
/// Lines must be separated by single LF.
113110
///
114-
/// Quote processing is not supported, it is assumed that they are
115-
/// deleted during simplification.
116-
///
117111
/// Signature separator line is not processed here, it is assumed to
118112
/// be stripped beforehand.
119113
pub fn unformat_flowed(text: &str, delsp: bool) -> String {
120114
let mut result = String::new();
121115
let mut skip_newline = true;
122116

123117
for line in text.split('\n') {
118+
let line = if !result.is_empty() && skip_newline {
119+
line.trim_start_matches('>')
120+
} else {
121+
line
122+
};
123+
124124
// Revert space-stuffing
125125
let line = line.strip_prefix(' ').unwrap_or(line);
126126

@@ -150,8 +150,20 @@ mod tests {
150150

151151
#[test]
152152
fn test_format_flowed() {
153+
let text = "";
154+
assert_eq!(format_flowed(text), "");
155+
153156
let text = "Foo bar baz";
154-
assert_eq!(format_flowed(text), "Foo bar baz");
157+
assert_eq!(format_flowed(text), text);
158+
159+
let text = ">Foo bar";
160+
assert_eq!(format_flowed(text), text);
161+
162+
let text = "> Foo bar";
163+
assert_eq!(format_flowed(text), text);
164+
165+
let text = ">\n\nA";
166+
assert_eq!(format_flowed(text), ">\r\n\r\nA");
155167

156168
let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\
157169
\n\
@@ -165,17 +177,33 @@ mod tests {
165177
let text = "> A quote";
166178
assert_eq!(format_flowed(text), "> A quote");
167179

180+
let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
181+
assert_eq!(
182+
format_flowed(text),
183+
"> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \r\n> A"
184+
);
185+
168186
// Test space stuffing of wrapped lines
169187
let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
170188
> \n\
171189
> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
172190
let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\
173191
> clients.\r\n\
174-
> \r\n\
192+
>\r\n\
175193
> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
176194
> client and enter the setup code presented on the generating device.";
177195
assert_eq!(format_flowed(text), expected);
178196

197+
let text = ">> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
198+
>> \n\
199+
>> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
200+
let expected = ">> This is the Autocrypt Setup Message used to transfer your key between \r\n\
201+
>> clients.\r\n\
202+
>>\r\n\
203+
>> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
204+
>> client and enter the setup code presented on the generating device.";
205+
assert_eq!(format_flowed(text), expected);
206+
179207
// Test space stuffing of spaces.
180208
let text = " Foo bar baz";
181209
assert_eq!(format_flowed(text), " Foo bar baz");
@@ -202,6 +230,12 @@ mod tests {
202230
let text = " Foo bar";
203231
let expected = " Foo bar";
204232
assert_eq!(unformat_flowed(text, false), expected);
233+
234+
let text =
235+
"> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \n> A";
236+
let expected =
237+
"> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
238+
assert_eq!(unformat_flowed(text, false), expected);
205239
}
206240

207241
#[test]

fuzz/fuzz_targets/fuzz_format_flowed.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ fn round_trip(input: &str) -> String {
1010
fn main() {
1111
check!().for_each(|data: &[u8]| {
1212
if let Ok(input) = std::str::from_utf8(data.into()) {
13-
let mut input = input.to_string();
14-
15-
// Only consider inputs that don't contain quotes.
16-
input.retain(|c| c != '>');
13+
let input = input.trim().to_string();
1714

1815
// Only consider inputs that are the result of unformatting format=flowed text.
1916
// At least this means that lines don't contain any trailing whitespace.

src/message.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2390,6 +2390,11 @@ mod tests {
23902390
let received = bob.recv_msg(&sent).await;
23912391
assert_eq!(received.text.as_deref(), Some(text));
23922392

2393+
let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
2394+
let sent = alice.send_text(chat.id, text).await;
2395+
let received = bob.recv_msg(&sent).await;
2396+
assert_eq!(received.text.as_deref(), Some(text));
2397+
23932398
let python_program = "\
23942399
def hello():
23952400
return 'Hello, world!'";

0 commit comments

Comments
 (0)