Skip to content

Commit b394a90

Browse files
committed
lint plain b"...".as_ptr() outside of CStr constructors
1 parent 99f725a commit b394a90

File tree

7 files changed

+155
-37
lines changed

7 files changed

+155
-37
lines changed

Diff for: book/src/lint_configuration.md

+1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ The minimum rust version that the project supports. Defaults to the `rust-versio
151151
* [`manual_try_fold`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_try_fold)
152152
* [`manual_hash_one`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_hash_one)
153153
* [`iter_kv_map`](https://rust-lang.github.io/rust-clippy/master/index.html#iter_kv_map)
154+
* [`manual_c_str_literals`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_c_str_literals)
154155

155156

156157
## `cognitive-complexity-threshold`

Diff for: clippy_config/src/conf.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ define_Conf! {
249249
///
250250
/// Suppress lints whenever the suggested change would cause breakage for other crates.
251251
(avoid_breaking_exported_api: bool = true),
252-
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, OPTION_MAP_UNWRAP_OR, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE, UNCHECKED_DURATION_SUBTRACTION, COLLAPSIBLE_STR_REPLACE, SEEK_FROM_CURRENT, SEEK_REWIND, UNNECESSARY_LAZY_EVALUATIONS, TRANSMUTE_PTR_TO_REF, ALMOST_COMPLETE_RANGE, NEEDLESS_BORROW, DERIVABLE_IMPLS, MANUAL_IS_ASCII_CHECK, MANUAL_REM_EUCLID, MANUAL_RETAIN, TYPE_REPETITION_IN_BOUNDS, TUPLE_ARRAY_CONVERSIONS, MANUAL_TRY_FOLD, MANUAL_HASH_ONE, ITER_KV_MAP.
252+
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, OPTION_MAP_UNWRAP_OR, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE, UNCHECKED_DURATION_SUBTRACTION, COLLAPSIBLE_STR_REPLACE, SEEK_FROM_CURRENT, SEEK_REWIND, UNNECESSARY_LAZY_EVALUATIONS, TRANSMUTE_PTR_TO_REF, ALMOST_COMPLETE_RANGE, NEEDLESS_BORROW, DERIVABLE_IMPLS, MANUAL_IS_ASCII_CHECK, MANUAL_REM_EUCLID, MANUAL_RETAIN, TYPE_REPETITION_IN_BOUNDS, TUPLE_ARRAY_CONVERSIONS, MANUAL_TRY_FOLD, MANUAL_HASH_ONE, ITER_KV_MAP, MANUAL_C_STR_LITERALS.
253253
///
254254
/// The minimum rust version that the project supports. Defaults to the `rust-version` field in `Cargo.toml`
255255
#[default_text = ""]

Diff for: clippy_lints/src/methods/manual_c_str_literals.rs

+102-19
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,75 @@ use clippy_utils::get_parent_expr;
44
use clippy_utils::source::snippet;
55
use rustc_ast::{LitKind, StrStyle};
66
use rustc_errors::Applicability;
7-
use rustc_hir::{Expr, ExprKind, QPath, TyKind};
7+
use rustc_hir::{Expr, ExprKind, Node, QPath, TyKind};
88
use rustc_lint::LateContext;
9-
use rustc_span::{sym, Span};
9+
use rustc_span::{sym, Span, Symbol};
1010

1111
use super::MANUAL_C_STR_LITERALS;
1212

13-
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, func: &Expr<'_>, args: &[Expr<'_>], msrv: &Msrv) {
13+
/// Checks:
14+
/// - `b"...".as_ptr()`
15+
/// - `b"...".as_ptr().cast()`
16+
/// - `"...".as_ptr()`
17+
/// - `"...".as_ptr().cast()`
18+
///
19+
/// Iff the parent call of `.cast()` isn't `CStr::from_ptr`, to avoid linting twice.
20+
pub(super) fn check_as_ptr<'tcx>(
21+
cx: &LateContext<'tcx>,
22+
expr: &'tcx Expr<'tcx>,
23+
receiver: &'tcx Expr<'tcx>,
24+
msrv: &Msrv,
25+
) {
26+
if let ExprKind::Lit(lit) = receiver.kind
27+
&& let LitKind::ByteStr(_, StrStyle::Cooked) | LitKind::Str(_, StrStyle::Cooked) = lit.node
28+
&& let casts_removed = peel_ptr_cast_ancestors(cx, expr)
29+
&& !get_parent_expr(cx, casts_removed).is_some_and(
30+
|parent| matches!(parent.kind, ExprKind::Call(func, _) if is_c_str_function(cx, func).is_some()),
31+
)
32+
&& let Some(sugg) = rewrite_as_cstr(cx, lit.span)
33+
&& msrv.meets(msrvs::C_STR_LITERALS)
34+
{
35+
span_lint_and_sugg(
36+
cx,
37+
MANUAL_C_STR_LITERALS,
38+
receiver.span,
39+
"manually constructing a nul-terminated string",
40+
r#"use a `c""` literal"#,
41+
sugg,
42+
// an additional cast may be needed, since the type of `CStr::as_ptr` and
43+
// `"".as_ptr()` can differ and is platform dependent
44+
Applicability::HasPlaceholders,
45+
);
46+
}
47+
}
48+
49+
/// Checks if the callee is a "relevant" `CStr` function considered by this lint.
50+
/// Returns the function name.
51+
fn is_c_str_function(cx: &LateContext<'_>, func: &Expr<'_>) -> Option<Symbol> {
1452
if let ExprKind::Path(QPath::TypeRelative(cstr, fn_name)) = &func.kind
1553
&& let TyKind::Path(QPath::Resolved(_, ty_path)) = &cstr.kind
1654
&& cx.tcx.lang_items().c_str() == ty_path.res.opt_def_id()
55+
{
56+
Some(fn_name.ident.name)
57+
} else {
58+
None
59+
}
60+
}
61+
62+
/// Checks calls to the `CStr` constructor functions:
63+
/// - `CStr::from_bytes_with_nul(..)`
64+
/// - `CStr::from_bytes_with_nul_unchecked(..)`
65+
/// - `CStr::from_ptr(..)`
66+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, func: &Expr<'_>, args: &[Expr<'_>], msrv: &Msrv) {
67+
if let Some(fn_name) = is_c_str_function(cx, func)
1768
&& let [arg] = args
1869
&& msrv.meets(msrvs::C_STR_LITERALS)
1970
{
20-
match fn_name.ident.name.as_str() {
71+
match fn_name.as_str() {
2172
name @ ("from_bytes_with_nul" | "from_bytes_with_nul_unchecked")
2273
if !arg.span.from_expansion()
2374
&& let ExprKind::Lit(lit) = arg.kind
24-
&& let LitKind::ByteStr(_, StrStyle::Cooked) = lit.node =>
75+
&& let LitKind::ByteStr(_, StrStyle::Cooked) | LitKind::Str(_, StrStyle::Cooked) = lit.node =>
2576
{
2677
check_from_bytes(cx, expr, arg, name);
2778
},
@@ -31,27 +82,27 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, func: &Expr<'_>, args
3182
}
3283
}
3384

34-
/// Checks `CStr::from_bytes_with_nul(b"foo\0")`
85+
/// Checks `CStr::from_ptr(b"foo\0".as_ptr().cast())`
3586
fn check_from_ptr(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>) {
36-
if let ExprKind::MethodCall(method, lit, [], _) = peel_ptr_cast(arg).kind
87+
if let ExprKind::MethodCall(method, lit, ..) = peel_ptr_cast(arg).kind
3788
&& method.ident.name == sym::as_ptr
3889
&& !lit.span.from_expansion()
3990
&& let ExprKind::Lit(lit) = lit.kind
4091
&& let LitKind::ByteStr(_, StrStyle::Cooked) = lit.node
92+
&& let Some(sugg) = rewrite_as_cstr(cx, lit.span)
4193
{
4294
span_lint_and_sugg(
4395
cx,
4496
MANUAL_C_STR_LITERALS,
4597
expr.span,
4698
"calling `CStr::from_ptr` with a byte string literal",
4799
r#"use a `c""` literal"#,
48-
rewrite_as_cstr(cx, lit.span),
100+
sugg,
49101
Applicability::MachineApplicable,
50102
);
51103
}
52104
}
53-
54-
/// Checks `CStr::from_ptr(b"foo\0".as_ptr().cast())`
105+
/// Checks `CStr::from_bytes_with_nul(b"foo\0")`
55106
fn check_from_bytes(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, method: &str) {
56107
let (span, applicability) = if let Some(parent) = get_parent_expr(cx, expr)
57108
&& let ExprKind::MethodCall(method, ..) = parent.kind
@@ -63,7 +114,11 @@ fn check_from_bytes(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, metho
63114
(expr.span, Applicability::MachineApplicable)
64115
} else {
65116
// User needs to remove error handling, can't be machine applicable
66-
(expr.span, Applicability::MaybeIncorrect)
117+
(expr.span, Applicability::HasPlaceholders)
118+
};
119+
120+
let Some(sugg) = rewrite_as_cstr(cx, arg.span) else {
121+
return;
67122
};
68123

69124
span_lint_and_sugg(
@@ -72,18 +127,19 @@ fn check_from_bytes(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, metho
72127
span,
73128
"calling `CStr::new` with a byte string literal",
74129
r#"use a `c""` literal"#,
75-
rewrite_as_cstr(cx, arg.span),
130+
sugg,
76131
applicability,
77132
);
78133
}
79134

80135
/// Rewrites a byte string literal to a c-str literal.
81136
/// `b"foo\0"` -> `c"foo"`
82-
pub fn rewrite_as_cstr(cx: &LateContext<'_>, span: Span) -> String {
137+
///
138+
/// Returns `None` if it doesn't end in a NUL byte.
139+
fn rewrite_as_cstr(cx: &LateContext<'_>, span: Span) -> Option<String> {
83140
let mut sugg = String::from("c") + snippet(cx, span.source_callsite(), "..").trim_start_matches('b');
84141

85142
// NUL byte should always be right before the closing quote.
86-
// (Can rfind ever return `None`?)
87143
if let Some(quote_pos) = sugg.rfind('"') {
88144
// Possible values right before the quote:
89145
// - literal NUL value
@@ -98,17 +154,44 @@ pub fn rewrite_as_cstr(cx: &LateContext<'_>, span: Span) -> String {
98154
else if sugg[..quote_pos].ends_with("\\0") {
99155
sugg.replace_range(quote_pos - 2..quote_pos, "");
100156
}
157+
// No known suffix, so assume it's not a C-string.
158+
else {
159+
return None;
160+
}
101161
}
102162

103-
sugg
163+
Some(sugg)
164+
}
165+
166+
fn get_cast_target<'tcx>(e: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
167+
match &e.kind {
168+
ExprKind::MethodCall(method, receiver, [], _) if method.ident.as_str() == "cast" => Some(receiver),
169+
ExprKind::Cast(expr, _) => Some(expr),
170+
_ => None,
171+
}
104172
}
105173

106174
/// `x.cast()` -> `x`
107175
/// `x as *const _` -> `x`
176+
/// `x` -> `x` (returns the same expression for non-cast exprs)
108177
fn peel_ptr_cast<'tcx>(e: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
109-
match &e.kind {
110-
ExprKind::MethodCall(method, receiver, [], _) if method.ident.as_str() == "cast" => peel_ptr_cast(receiver),
111-
ExprKind::Cast(expr, _) => peel_ptr_cast(expr),
112-
_ => e,
178+
get_cast_target(e).map_or(e, peel_ptr_cast)
179+
}
180+
181+
/// Same as `peel_ptr_cast`, but the other way around, by walking up the ancestor cast expressions:
182+
///
183+
/// `foo(x.cast() as *const _)`
184+
/// ^ given this `x` expression, returns the `foo(...)` expression
185+
fn peel_ptr_cast_ancestors<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
186+
let mut prev = e;
187+
for (_, node) in cx.tcx.hir().parent_iter(e.hir_id) {
188+
if let Node::Expr(e) = node
189+
&& get_cast_target(e).is_some()
190+
{
191+
prev = e;
192+
} else {
193+
break;
194+
}
113195
}
196+
prev
114197
}

Diff for: clippy_lints/src/methods/mod.rs

+15-7
Original file line numberDiff line numberDiff line change
@@ -3755,24 +3755,31 @@ declare_clippy_lint! {
37553755

37563756
declare_clippy_lint! {
37573757
/// ### What it does
3758-
/// Checks for calls to `CStr::from_ptr` and `CStr::from_bytes_with_nul` with byte string literals as arguments.
3758+
/// Checks for the manual creation of C strings (a string with a `NUL` byte at the end), either
3759+
/// through one of the `CStr` constructor functions, or more plainly by calling `.as_ptr()`
3760+
/// on a (byte) string literal with a hardcoded `\0` byte at the end.
37593761
///
37603762
/// ### Why is this bad?
37613763
/// This can be written more concisely using `c"str"` literals and is also less error-prone,
3762-
/// because the compiler checks for interior nul bytes.
3764+
/// because the compiler checks for interior `NUL` bytes and the terminating `NUL` byte is inserted automatically.
37633765
///
37643766
/// ### Example
37653767
/// ```no_run
37663768
/// # use std::ffi::CStr;
3767-
/// # fn needs_cstr(_: &CStr) {}
3768-
/// needs_cstr(CStr::from_bytes_with_nul(b":)").unwrap());
3769+
/// # mod libc { pub unsafe fn puts(_: *const i8) {} }
3770+
/// fn needs_cstr(_: &CStr) {}
3771+
///
3772+
/// needs_cstr(CStr::from_bytes_with_nul(b"Hello\0").unwrap());
3773+
/// unsafe { libc::puts("World\0".as_ptr().cast()) }
37693774
/// ```
37703775
/// Use instead:
37713776
/// ```no_run
3772-
/// # #![feature(c_str_literals)]
37733777
/// # use std::ffi::CStr;
3774-
/// # fn needs_cstr(_: &CStr) {}
3775-
/// needs_cstr(c":)");
3778+
/// # mod libc { pub unsafe fn puts(_: *const i8) {} }
3779+
/// fn needs_cstr(_: &CStr) {}
3780+
///
3781+
/// needs_cstr(c"Hello");
3782+
/// unsafe { libc::puts(c"World".as_ptr()) }
37763783
/// ```
37773784
#[clippy::version = "1.76.0"]
37783785
pub MANUAL_C_STR_LITERALS,
@@ -4178,6 +4185,7 @@ impl Methods {
41784185
}
41794186
},
41804187
("as_mut", []) => useless_asref::check(cx, expr, "as_mut", recv),
4188+
("as_ptr", []) => manual_c_str_literals::check_as_ptr(cx, expr, recv, &self.msrv),
41814189
("as_ref", []) => useless_asref::check(cx, expr, "as_ref", recv),
41824190
("assume_init", []) => uninit_assumed_init::check(cx, expr, recv),
41834191
("cloned", []) => cloned_instead_of_copied::check(cx, expr, recv, span, &self.msrv),

Diff for: tests/ui/manual_c_str_literals.fixed

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#![feature(c_str_literals)] // TODO: remove in the next sync
21
#![warn(clippy::manual_c_str_literals)]
32
#![allow(clippy::no_effect)]
43

@@ -43,6 +42,11 @@ fn main() {
4342

4443
unsafe { c"foo" };
4544
unsafe { c"foo" };
45+
let _: *const _ = c"foo".as_ptr();
46+
let _: *const _ = c"foo".as_ptr();
47+
let _: *const _ = "foo".as_ptr(); // not a C-string
48+
let _: *const _ = "".as_ptr();
49+
let _: *const _ = c"foo".as_ptr().cast::<i8>();
4650

4751
// Macro cases, don't lint:
4852
cstr!("foo");

Diff for: tests/ui/manual_c_str_literals.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#![feature(c_str_literals)] // TODO: remove in the next sync
21
#![warn(clippy::manual_c_str_literals)]
32
#![allow(clippy::no_effect)]
43

@@ -43,6 +42,11 @@ fn main() {
4342

4443
unsafe { CStr::from_ptr(b"foo\0".as_ptr().cast()) };
4544
unsafe { CStr::from_ptr(b"foo\0".as_ptr() as *const _) };
45+
let _: *const _ = b"foo\0".as_ptr();
46+
let _: *const _ = "foo\0".as_ptr();
47+
let _: *const _ = "foo".as_ptr(); // not a C-string
48+
let _: *const _ = "".as_ptr();
49+
let _: *const _ = b"foo\0".as_ptr().cast::<i8>();
4650

4751
// Macro cases, don't lint:
4852
cstr!("foo");

Diff for: tests/ui/manual_c_str_literals.stderr

+26-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: calling `CStr::new` with a byte string literal
2-
--> $DIR/manual_c_str_literals.rs:32:5
2+
--> $DIR/manual_c_str_literals.rs:31:5
33
|
44
LL | CStr::from_bytes_with_nul(b"foo\0");
55
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use a `c""` literal: `c"foo"`
@@ -8,40 +8,58 @@ LL | CStr::from_bytes_with_nul(b"foo\0");
88
= help: to override `-D warnings` add `#[allow(clippy::manual_c_str_literals)]`
99

1010
error: calling `CStr::new` with a byte string literal
11-
--> $DIR/manual_c_str_literals.rs:36:5
11+
--> $DIR/manual_c_str_literals.rs:35:5
1212
|
1313
LL | CStr::from_bytes_with_nul(b"foo\0");
1414
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use a `c""` literal: `c"foo"`
1515

1616
error: calling `CStr::new` with a byte string literal
17-
--> $DIR/manual_c_str_literals.rs:37:5
17+
--> $DIR/manual_c_str_literals.rs:36:5
1818
|
1919
LL | CStr::from_bytes_with_nul(b"foo\x00");
2020
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use a `c""` literal: `c"foo"`
2121

2222
error: calling `CStr::new` with a byte string literal
23-
--> $DIR/manual_c_str_literals.rs:38:5
23+
--> $DIR/manual_c_str_literals.rs:37:5
2424
|
2525
LL | CStr::from_bytes_with_nul(b"foo\0").unwrap();
2626
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use a `c""` literal: `c"foo"`
2727

2828
error: calling `CStr::new` with a byte string literal
29-
--> $DIR/manual_c_str_literals.rs:39:5
29+
--> $DIR/manual_c_str_literals.rs:38:5
3030
|
3131
LL | CStr::from_bytes_with_nul(b"foo\\0sdsd\0").unwrap();
3232
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use a `c""` literal: `c"foo\\0sdsd"`
3333

3434
error: calling `CStr::from_ptr` with a byte string literal
35-
--> $DIR/manual_c_str_literals.rs:44:14
35+
--> $DIR/manual_c_str_literals.rs:43:14
3636
|
3737
LL | unsafe { CStr::from_ptr(b"foo\0".as_ptr().cast()) };
3838
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use a `c""` literal: `c"foo"`
3939

4040
error: calling `CStr::from_ptr` with a byte string literal
41-
--> $DIR/manual_c_str_literals.rs:45:14
41+
--> $DIR/manual_c_str_literals.rs:44:14
4242
|
4343
LL | unsafe { CStr::from_ptr(b"foo\0".as_ptr() as *const _) };
4444
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use a `c""` literal: `c"foo"`
4545

46-
error: aborting due to 7 previous errors
46+
error: manually constructing a nul-terminated string
47+
--> $DIR/manual_c_str_literals.rs:45:23
48+
|
49+
LL | let _: *const _ = b"foo\0".as_ptr();
50+
| ^^^^^^^^ help: use a `c""` literal: `c"foo"`
51+
52+
error: manually constructing a nul-terminated string
53+
--> $DIR/manual_c_str_literals.rs:46:23
54+
|
55+
LL | let _: *const _ = "foo\0".as_ptr();
56+
| ^^^^^^^ help: use a `c""` literal: `c"foo"`
57+
58+
error: manually constructing a nul-terminated string
59+
--> $DIR/manual_c_str_literals.rs:49:23
60+
|
61+
LL | let _: *const _ = b"foo\0".as_ptr().cast::<i8>();
62+
| ^^^^^^^^ help: use a `c""` literal: `c"foo"`
63+
64+
error: aborting due to 10 previous errors
4765

0 commit comments

Comments
 (0)