Skip to content

Commit e7d1658

Browse files
committed
Escape fewer Unicode codepoints in Debug impl of str
Use the same procedure as Python to determine whether a character is printable, described in [PEP 3138]. In particular, this means that the following character classes are escaped: - Cc (Other, Control) - Cf (Other, Format) - Cs (Other, Surrogate), even though they can't appear in Rust strings - Co (Other, Private Use) - Cn (Other, Not Assigned) - Zl (Separator, Line) - Zp (Separator, Paragraph) - Zs (Separator, Space), except for the ASCII space `' '` (`0x20`) This allows for user-friendly inspection of strings that are not English (e.g. compare `"\u{e9}\u{e8}\u{ea}"` to `"éèê"`). Fixes rust-lang#34318. [PEP 3138]: https://www.python.org/dev/peps/pep-3138/
1 parent ad264f7 commit e7d1658

File tree

6 files changed

+872
-9
lines changed

6 files changed

+872
-9
lines changed

src/etc/char_private.py

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2011-2016 The Rust Project Developers. See the COPYRIGHT
4+
# file at the top-level directory of this distribution and at
5+
# http://rust-lang.org/COPYRIGHT.
6+
#
7+
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
8+
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
9+
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
10+
# option. This file may not be copied, modified, or distributed
11+
# except according to those terms.
12+
13+
# This script uses the following Unicode tables:
14+
# - Categories.txt
15+
16+
import os
17+
import subprocess
18+
19+
def to_ranges(iter):
20+
current = None
21+
for i in iter:
22+
if current is None or i != current[1] or i in (0x10000, 0x20000):
23+
if current is not None:
24+
yield tuple(current)
25+
current = [i, i + 1]
26+
else:
27+
current[1] += 1
28+
if current is not None:
29+
yield tuple(current)
30+
31+
def get_escaped(dictionary):
32+
for i in range(0x110000):
33+
if dictionary.get(i, "Cn") in "Cc Cf Cs Co Cn Zl Zp Zs".split() and i != ord(' '):
34+
yield i
35+
36+
def get_file(f):
37+
try:
38+
return open(os.path.basename(f))
39+
except FileNotFoundError:
40+
subprocess.run(["curl", "-O", f], check=True)
41+
return open(os.path.basename(f))
42+
43+
def main():
44+
file = get_file("http://www.unicode.org/notes/tn36/Categories.txt")
45+
46+
dictionary = {int(line.split()[0], 16): line.split()[1] for line in file}
47+
48+
CUTOFF=0x10000
49+
singletons0 = []
50+
singletons1 = []
51+
normal0 = []
52+
normal1 = []
53+
extra = []
54+
55+
for a, b in to_ranges(get_escaped(dictionary)):
56+
if a > 2 * CUTOFF:
57+
extra.append((a, b - a))
58+
elif a == b - 1:
59+
if a & CUTOFF:
60+
singletons1.append(a & ~CUTOFF)
61+
else:
62+
singletons0.append(a)
63+
elif a == b - 2:
64+
if a & CUTOFF:
65+
singletons1.append(a & ~CUTOFF)
66+
singletons1.append((a + 1) & ~CUTOFF)
67+
else:
68+
singletons0.append(a)
69+
singletons0.append(a + 1)
70+
else:
71+
if a >= 2 * CUTOFF:
72+
extra.append((a, b - a))
73+
elif a & CUTOFF:
74+
normal1.append((a & ~CUTOFF, b - a))
75+
else:
76+
normal0.append((a, b - a))
77+
78+
print("""\
79+
// Copyright 2012-2016 The Rust Project Developers. See the COPYRIGHT
80+
// file at the top-level directory of this distribution and at
81+
// http://rust-lang.org/COPYRIGHT.
82+
//
83+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
84+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
85+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
86+
// option. This file may not be copied, modified, or distributed
87+
// except according to those terms.
88+
89+
// NOTE: The following code was generated by "src/etc/char_private.py",
90+
// do not edit directly!
91+
92+
use slice::SliceExt;
93+
94+
fn check(x: u16, singletons: &[u16], normal: &[u16]) -> bool {
95+
for &s in singletons {
96+
if x == s {
97+
return false;
98+
} else if x < s {
99+
break;
100+
}
101+
}
102+
for w in normal.chunks(2) {
103+
let start = w[0];
104+
let len = w[1];
105+
let difference = (x as i32) - (start as i32);
106+
if 0 <= difference {
107+
if difference < len as i32 {
108+
return false;
109+
}
110+
} else {
111+
break;
112+
}
113+
}
114+
true
115+
}
116+
117+
pub fn is_printable(x: char) -> bool {
118+
let x = x as u32;
119+
let lower = x as u16;
120+
if x < 0x10000 {
121+
check(lower, SINGLETONS0, NORMAL0)
122+
} else if x < 0x20000 {
123+
check(lower, SINGLETONS1, NORMAL1)
124+
} else {\
125+
""")
126+
for a, b in extra:
127+
print(" if 0x{:x} <= x && x < 0x{:x} {{".format(a, a + b))
128+
print(" return false;")
129+
print(" }")
130+
print("""\
131+
true
132+
}
133+
}\
134+
""")
135+
print()
136+
print("const SINGLETONS0: &'static [u16] = &[")
137+
for s in singletons0:
138+
print(" 0x{:x},".format(s))
139+
print("];")
140+
print("const SINGLETONS1: &'static [u16] = &[")
141+
for s in singletons1:
142+
print(" 0x{:x},".format(s))
143+
print("];")
144+
print("const NORMAL0: &'static [u16] = &[")
145+
for a, b in normal0:
146+
print(" 0x{:x}, 0x{:x},".format(a, b))
147+
print("];")
148+
print("const NORMAL1: &'static [u16] = &[")
149+
for a, b in normal1:
150+
print(" 0x{:x}, 0x{:x},".format(a, b))
151+
print("];")
152+
153+
if __name__ == '__main__':
154+
main()

src/libcollectionstest/str.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -707,12 +707,14 @@ fn test_escape_unicode() {
707707
fn test_escape_default() {
708708
assert_eq!("abc".escape_default(), "abc");
709709
assert_eq!("a c".escape_default(), "a c");
710+
assert_eq!("éèê".escape_default(), "éèê");
710711
assert_eq!("\r\n\t".escape_default(), "\\r\\n\\t");
711712
assert_eq!("'\"\\".escape_default(), "\\'\\\"\\\\");
712-
assert_eq!("\u{100}\u{ffff}".escape_default(), "\\u{100}\\u{ffff}");
713-
assert_eq!("\u{10000}\u{10ffff}".escape_default(), "\\u{10000}\\u{10ffff}");
714-
assert_eq!("ab\u{fb00}".escape_default(), "ab\\u{fb00}");
715-
assert_eq!("\u{1d4ea}\r".escape_default(), "\\u{1d4ea}\\r");
713+
assert_eq!("\u{7f}\u{ff}".escape_default(), "\\u{7f}\u{ff}");
714+
assert_eq!("\u{100}\u{ffff}".escape_default(), "\u{100}\\u{ffff}");
715+
assert_eq!("\u{10000}\u{10ffff}".escape_default(), "\u{10000}\\u{10ffff}");
716+
assert_eq!("ab\u{200b}".escape_default(), "ab\\u{200b}");
717+
assert_eq!("\u{10d4ea}\r".escape_default(), "\\u{10d4ea}\\r");
716718
}
717719

718720
#[test]

src/libcore/char.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
use prelude::v1::*;
1919

20+
use char_private::is_printable;
2021
use mem::transmute;
2122

2223
// UTF-8 ranges and tags for encoding characters
@@ -320,8 +321,8 @@ impl CharExt for char {
320321
'\r' => EscapeDefaultState::Backslash('r'),
321322
'\n' => EscapeDefaultState::Backslash('n'),
322323
'\\' | '\'' | '"' => EscapeDefaultState::Backslash(self),
323-
'\x20' ... '\x7e' => EscapeDefaultState::Char(self),
324-
_ => EscapeDefaultState::Unicode(self.escape_unicode())
324+
c if is_printable(c) => EscapeDefaultState::Char(c),
325+
c => EscapeDefaultState::Unicode(c.escape_unicode()),
325326
};
326327
EscapeDefault { state: init_state }
327328
}

0 commit comments

Comments
 (0)