Skip to content

Commit d50dcd9

Browse files
authored
Merge pull request #450 from dtolnay/cstr
Add `Literal::c_string` constructor
2 parents 45730bc + 70a804b commit d50dcd9

File tree

5 files changed

+106
-22
lines changed

5 files changed

+106
-22
lines changed

build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ fn main() {
6565

6666
if rustc < 79 {
6767
println!("cargo:rustc-cfg=no_literal_byte_character");
68+
println!("cargo:rustc-cfg=no_literal_c_string");
6869
}
6970

7071
if !cfg!(feature = "proc-macro") {

src/fallback.rs

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use core::mem::ManuallyDrop;
1515
use core::ops::Range;
1616
use core::ops::RangeBounds;
1717
use core::ptr;
18-
use core::str::FromStr;
18+
use core::str::{self, FromStr};
19+
use std::ffi::CStr;
1920
#[cfg(procmacro2_semver_exempt)]
2021
use std::path::PathBuf;
2122

@@ -1010,27 +1011,7 @@ impl Literal {
10101011
pub fn string(string: &str) -> Literal {
10111012
let mut repr = String::with_capacity(string.len() + 2);
10121013
repr.push('"');
1013-
let mut chars = string.chars();
1014-
while let Some(ch) = chars.next() {
1015-
if ch == '\0' {
1016-
repr.push_str(
1017-
if chars
1018-
.as_str()
1019-
.starts_with(|next| '0' <= next && next <= '7')
1020-
{
1021-
// circumvent clippy::octal_escapes lint
1022-
r"\x00"
1023-
} else {
1024-
r"\0"
1025-
},
1026-
);
1027-
} else if ch == '\'' {
1028-
// escape_debug turns this into "\'" which is unnecessary.
1029-
repr.push(ch);
1030-
} else {
1031-
repr.extend(ch.escape_debug());
1032-
}
1033-
}
1014+
escape_utf8(string, &mut repr);
10341015
repr.push('"');
10351016
Literal::_new(repr)
10361017
}
@@ -1093,6 +1074,34 @@ impl Literal {
10931074
Literal::_new(repr)
10941075
}
10951076

1077+
pub fn c_string(string: &CStr) -> Literal {
1078+
let mut repr = "c\"".to_string();
1079+
let mut bytes = string.to_bytes();
1080+
while !bytes.is_empty() {
1081+
let (valid, invalid) = match str::from_utf8(bytes) {
1082+
Ok(all_valid) => {
1083+
bytes = b"";
1084+
(all_valid, bytes)
1085+
}
1086+
Err(utf8_error) => {
1087+
let (valid, rest) = bytes.split_at(utf8_error.valid_up_to());
1088+
let valid = str::from_utf8(valid).unwrap();
1089+
let invalid = utf8_error
1090+
.error_len()
1091+
.map_or(rest, |error_len| &rest[..error_len]);
1092+
bytes = &bytes[valid.len() + invalid.len()..];
1093+
(valid, invalid)
1094+
}
1095+
};
1096+
escape_utf8(valid, &mut repr);
1097+
for &byte in invalid {
1098+
let _ = write!(repr, r"\x{:02X}", byte);
1099+
}
1100+
}
1101+
repr.push('"');
1102+
Literal::_new(repr)
1103+
}
1104+
10961105
pub fn span(&self) -> Span {
10971106
self.span
10981107
}
@@ -1191,3 +1200,27 @@ impl Debug for Literal {
11911200
debug.finish()
11921201
}
11931202
}
1203+
1204+
fn escape_utf8(string: &str, repr: &mut String) {
1205+
let mut chars = string.chars();
1206+
while let Some(ch) = chars.next() {
1207+
if ch == '\0' {
1208+
repr.push_str(
1209+
if chars
1210+
.as_str()
1211+
.starts_with(|next| '0' <= next && next <= '7')
1212+
{
1213+
// circumvent clippy::octal_escapes lint
1214+
r"\x00"
1215+
} else {
1216+
r"\0"
1217+
},
1218+
);
1219+
} else if ch == '\'' {
1220+
// escape_debug turns this into "\'" which is unnecessary.
1221+
repr.push(ch);
1222+
} else {
1223+
repr.extend(ch.escape_debug());
1224+
}
1225+
}
1226+
}

src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ use core::ops::Range;
170170
use core::ops::RangeBounds;
171171
use core::str::FromStr;
172172
use std::error::Error;
173+
use std::ffi::CStr;
173174
#[cfg(procmacro2_semver_exempt)]
174175
use std::path::PathBuf;
175176

@@ -1244,6 +1245,11 @@ impl Literal {
12441245
Literal::_new(imp::Literal::byte_string(bytes))
12451246
}
12461247

1248+
/// C string literal.
1249+
pub fn c_string(string: &CStr) -> Literal {
1250+
Literal::_new(imp::Literal::c_string(string))
1251+
}
1252+
12471253
/// Returns the span encompassing this literal.
12481254
pub fn span(&self) -> Span {
12491255
Span::_new(self.inner.span())

src/wrapper.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use core::fmt::{self, Debug, Display};
77
use core::ops::Range;
88
use core::ops::RangeBounds;
99
use core::str::FromStr;
10+
use std::ffi::CStr;
1011
use std::panic;
1112
#[cfg(super_unstable)]
1213
use std::path::PathBuf;
@@ -889,6 +890,25 @@ impl Literal {
889890
}
890891
}
891892

893+
pub fn c_string(string: &CStr) -> Literal {
894+
if inside_proc_macro() {
895+
Literal::Compiler({
896+
#[cfg(not(no_literal_c_string))]
897+
{
898+
proc_macro::Literal::c_string(string)
899+
}
900+
901+
#[cfg(no_literal_c_string)]
902+
{
903+
let fallback = fallback::Literal::c_string(string);
904+
fallback.repr.parse::<proc_macro::Literal>().unwrap()
905+
}
906+
})
907+
} else {
908+
Literal::Fallback(fallback::Literal::c_string(string))
909+
}
910+
}
911+
892912
pub fn span(&self) -> Span {
893913
match self {
894914
Literal::Compiler(lit) => Span::Compiler(lit.span()),

tests/test.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
)]
77

88
use proc_macro2::{Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
9+
use std::ffi::CStr;
910
use std::iter;
1011
use std::str::{self, FromStr};
1112

@@ -164,6 +165,29 @@ fn literal_byte_string() {
164165

165166
#[test]
166167
fn literal_c_string() {
168+
assert_eq!(Literal::c_string(<&CStr>::default()).to_string(), "c\"\"");
169+
170+
let cstr = CStr::from_bytes_with_nul(b"aA\0").unwrap();
171+
assert_eq!(Literal::c_string(cstr).to_string(), r#" c"aA" "#.trim());
172+
173+
let cstr = CStr::from_bytes_with_nul(b"\t\0").unwrap();
174+
assert_eq!(Literal::c_string(cstr).to_string(), r#" c"\t" "#.trim());
175+
176+
let cstr = CStr::from_bytes_with_nul(b"\xE2\x9D\xA4\0").unwrap();
177+
assert_eq!(Literal::c_string(cstr).to_string(), r#" c"❤" "#.trim());
178+
179+
let cstr = CStr::from_bytes_with_nul(b"'\0").unwrap();
180+
assert_eq!(Literal::c_string(cstr).to_string(), r#" c"'" "#.trim());
181+
182+
let cstr = CStr::from_bytes_with_nul(b"\"\0").unwrap();
183+
assert_eq!(Literal::c_string(cstr).to_string(), r#" c"\"" "#.trim());
184+
185+
let cstr = CStr::from_bytes_with_nul(b"\x7F\xFF\xFE\xCC\xB3\0").unwrap();
186+
assert_eq!(
187+
Literal::c_string(cstr).to_string(),
188+
r#" c"\u{7f}\xFF\xFE\u{333}" "#.trim(),
189+
);
190+
167191
let strings = r###"
168192
c"hello\x80我叫\u{1F980}" // from the RFC
169193
cr"\"

0 commit comments

Comments
 (0)