Skip to content

Commit 2b64df0

Browse files
peffgitster
authored andcommitted
skip_prefix(): check const match between in and out params
The skip_prefix() function takes in a "const char *" string, and returns via a "const char **" out-parameter that points somewhere in that string. This is fine if you are operating on a const string, like: const char *in = ...; const char *out; if (skip_prefix(in, "foo", &out)) ...look at out... It is also OK if "in" is not const but "out" is, as we add an implicit const when we pass "in" to the function. But there's another case where this is limiting. If we want both fields to be non-const, like: char *in = ...; char *out; if (skip_prefix(in, "foo", &out)) *out = '\0'; it doesn't work. The compiler will complain about the type mismatch in passing "&out" to a parameter which expects "const char **". So to make this work, we have to do an explicit cast. But such a cast is ugly, and also means that we run afoul of making this mistake: const char *in = ...; char *out; if (skip_prefix(in, "foo", (const char **)&out)) *out = '\0'; which causes us to write to the memory pointed by "in", which was const. We can imagine these four cases as: (1) const in, const out (2) non-const in, const out (3) non-const in, non-const out (4) const in, non-const out Cases (1) and (2) work now. We would like case (3) to work but it doesn't. But we would like to catch case (4) as a compile error. So ideally the rule is "the out-parameter must be at least as const as the in-parameter". We can do this with some macro trickery. We wrap skip_prefix() in a macro so that it has access to the real types of in/out. And then we pass those parameters through another macro which: 1. Fails if the "at least as const" rule is not filled. 2. Casts to match the signature of the real skip_prefix(). There are a lot of ways to implement the "fails" part. You can use __builtin_types_compatible_p() to check, and then either our BUILD_ASSERT macros or _Static_assert to fail. But that requires some conditional compilation based on compiler feature. That's probably OK (the fallback would be to just cast without catching case 4). But we can do better. The macro I have here uses a ternary with a dead branch that tries to assign "in" to "out", which should work everywhere and lets the compiler catch the problem in the usual way. With an input like this: int foo(const char *x, const char **y); #define foo(in,out) foo((in), CONST_OUTPARAM((in), (out))) void ok_const(const char *x, const char **y) { foo(x, y); } void ok_nonconst(char *x, char **y) { foo(x, y); } void ok_add_const(char *x, const char **y) { foo(x, y); } void bad_drop_const(const char *x, char **y) { foo(x, y); } gcc reports: foo.c: In function ‘bad_drop_const’: foo.c:2:35: error: assignment discards ‘const’ qualifier from pointer target type [-Werror=discarded-qualifiers] 2 | ((const char **)(0 ? ((*(out) = (in)),(out)) : (out))) | ^ foo.c:4:31: note: in expansion of macro ‘CONST_OUTPARAM’ 4 | #define foo(in,out) foo((in), CONST_OUTPARAM((in), (out))) | ^~~~~~~~~~~~~~ foo.c:23:9: note: in expansion of macro ‘foo’ 23 | foo(x, y); | ^~~ It's a bit verbose, but I think makes it reasonably clear what's going on. Using BUILD_ASSERT_OR_ZERO() ends up much worse. Using _Static_assert you can be a bit more informative, but that's not something we use at all yet in our code-base (it's an old gnu-ism later standardized in C11). Our generic macro only works for "const char **", which is something we could improve by using typeof(in). But that introduces more portability questions, and also some weird corner cases (e.g., around implicit void conversion). This patch just introduces the concept. We'll make use of it in future patches. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 97eee5a commit 2b64df0

1 file changed

Lines changed: 17 additions & 0 deletions

File tree

git-compat-util.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,23 @@ static inline bool skip_prefix(const char *str, const char *prefix,
491491
return false;
492492
}
493493

494+
/*
495+
* Check that an out-parameter that is "at least as const as" a matching
496+
* in-parameter. For example, skip_prefix() will return "out" that is a subset
497+
* of "str". So:
498+
*
499+
* const str, const out: ok
500+
* non-const str, const out: ok
501+
* non-const str, non-const out: ok
502+
* const str, non-const out: compile error
503+
*
504+
* See the skip_prefix macro below for an example of use.
505+
*/
506+
#define CONST_OUTPARAM(in, out) \
507+
((const char **)(0 ? ((*(out) = (in)),(out)) : (out)))
508+
#define skip_prefix(str, prefix, out) \
509+
skip_prefix((str), (prefix), CONST_OUTPARAM((str), (out)))
510+
494511
/*
495512
* Like skip_prefix, but promises never to read past "len" bytes of the input
496513
* buffer, and returns the remaining number of bytes in "out" via "outlen".

0 commit comments

Comments
 (0)