PR middle-end/77735 - FAIL: gcc.dg/tree-ssa/builtin-sprintf-warn-1.c
gcc/ChangeLog: PR middle-end/77735 * builtins.c (string_length): New function. (c_strlen): Use string_length. Correctly handle wide strings. * gimple-ssa-sprintf.c (target_max_value, target_size_max): New functions. (target_int_max): Call target_max_value. (format_result::knownrange): New data member. (fmtresult::fmtresult): Define default constructor. (format_integer): Use it and set format_result::knownrange. Handle global constants. (format_floating_max): Add third argument. (format_floating): Recompute maximum value for %a for each argument. (get_string_length): Use fmtresult default ctor. (format_string): Set format_result::knownrange. (format_directive): Check format_result::knownrange. (add_bytes): Same. Correct caret placement in diagnostics. (pass_sprintf_length::compute_format_length): Set format_result::knownrange. (pass_sprintf_length::handle_gimple_call): Use target_size_max. gcc/testsuite/ChangeLog: PR middle-end/77735 * gcc.dg/tree-ssa/builtin-sprintf-2.c: Add test cases. * gcc.dg/tree-ssa/builtin-sprintf-warn-1.c: Same. * gcc.dg/tree-ssa/builtin-sprintf-warn-2.c: Same. * gcc.dg/tree-ssa/builtin-sprintf-warn-3.c: Adjust/relax. * gcc.dg/tree-ssa/builtin-sprintf-warn-4.c: Add test cases. * gcc.dg/tree-ssa/builtin-sprintf-warn-6.c: XFAIL for LP64 only. * gcc.dg/tree-ssa/builtin-sprintf.c: Add test cases. From-SVN: r241489
This commit is contained in:
parent
94caf86019
commit
1eb4547b10
11 changed files with 558 additions and 199 deletions
|
@ -1,3 +1,25 @@
|
|||
2016-10-24 Martin Sebor <msebor@redhat.com>
|
||||
|
||||
PR middle-end/77735
|
||||
* builtins.c (string_length): New function.
|
||||
(c_strlen): Use string_length. Correctly handle wide strings.
|
||||
* gimple-ssa-sprintf.c (target_max_value, target_size_max): New
|
||||
functions.
|
||||
(target_int_max): Call target_max_value.
|
||||
(format_result::knownrange): New data member.
|
||||
(fmtresult::fmtresult): Define default constructor.
|
||||
(format_integer): Use it and set format_result::knownrange.
|
||||
Handle global constants.
|
||||
(format_floating_max): Add third argument.
|
||||
(format_floating): Recompute maximum value for %a for each argument.
|
||||
(get_string_length): Use fmtresult default ctor.
|
||||
(format_string): Set format_result::knownrange.
|
||||
(format_directive): Check format_result::knownrange.
|
||||
(add_bytes): Same. Correct caret placement in diagnostics.
|
||||
(pass_sprintf_length::compute_format_length): Set
|
||||
format_result::knownrange.
|
||||
(pass_sprintf_length::handle_gimple_call): Use target_size_max.
|
||||
|
||||
2016-10-24 Jakub Jelinek <jakub@redhat.com>
|
||||
|
||||
* config/i386/i386.c (ix86_in_large_data_p, ix86_expand_builtin): Use
|
||||
|
|
103
gcc/builtins.c
103
gcc/builtins.c
|
@ -503,9 +503,44 @@ get_pointer_alignment (tree exp)
|
|||
return align;
|
||||
}
|
||||
|
||||
/* Compute the length of a C string. TREE_STRING_LENGTH is not the right
|
||||
way, because it could contain a zero byte in the middle.
|
||||
TREE_STRING_LENGTH is the size of the character array, not the string.
|
||||
/* Return the number of non-zero elements in the sequence
|
||||
[ PTR, PTR + MAXELTS ) where each element's size is ELTSIZE bytes.
|
||||
ELTSIZE must be a power of 2 less than 8. Used by c_strlen. */
|
||||
|
||||
static unsigned
|
||||
string_length (const void *ptr, unsigned eltsize, unsigned maxelts)
|
||||
{
|
||||
gcc_checking_assert (eltsize == 1 || eltsize == 2 || eltsize == 4);
|
||||
|
||||
unsigned n;
|
||||
|
||||
if (eltsize == 1)
|
||||
{
|
||||
/* Optimize the common case of plain char. */
|
||||
for (n = 0; n < maxelts; n++)
|
||||
{
|
||||
const char *elt = (const char*) ptr + n;
|
||||
if (!*elt)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (n = 0; n < maxelts; n++)
|
||||
{
|
||||
const char *elt = (const char*) ptr + n * eltsize;
|
||||
if (!memcmp (elt, "\0\0\0\0", eltsize))
|
||||
break;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Compute the length of a null-terminated character string or wide
|
||||
character string handling character sizes of 1, 2, and 4 bytes.
|
||||
TREE_STRING_LENGTH is not the right way because it evaluates to
|
||||
the size of the character array in bytes (as opposed to characters)
|
||||
and because it can contain a zero byte in the middle.
|
||||
|
||||
ONLY_VALUE should be nonzero if the result is not going to be emitted
|
||||
into the instruction stream and zero if it is going to be expanded.
|
||||
|
@ -526,12 +561,6 @@ get_pointer_alignment (tree exp)
|
|||
tree
|
||||
c_strlen (tree src, int only_value)
|
||||
{
|
||||
tree offset_node;
|
||||
HOST_WIDE_INT offset;
|
||||
int max;
|
||||
const char *ptr;
|
||||
location_t loc;
|
||||
|
||||
STRIP_NOPS (src);
|
||||
if (TREE_CODE (src) == COND_EXPR
|
||||
&& (only_value || !TREE_SIDE_EFFECTS (TREE_OPERAND (src, 0))))
|
||||
|
@ -548,25 +577,36 @@ c_strlen (tree src, int only_value)
|
|||
&& (only_value || !TREE_SIDE_EFFECTS (TREE_OPERAND (src, 0))))
|
||||
return c_strlen (TREE_OPERAND (src, 1), only_value);
|
||||
|
||||
loc = EXPR_LOC_OR_LOC (src, input_location);
|
||||
location_t loc = EXPR_LOC_OR_LOC (src, input_location);
|
||||
|
||||
src = string_constant (src, &offset_node);
|
||||
/* Offset from the beginning of the string in bytes. */
|
||||
tree byteoff;
|
||||
src = string_constant (src, &byteoff);
|
||||
if (src == 0)
|
||||
return NULL_TREE;
|
||||
|
||||
max = TREE_STRING_LENGTH (src) - 1;
|
||||
ptr = TREE_STRING_POINTER (src);
|
||||
/* Determine the size of the string element. */
|
||||
unsigned eltsize
|
||||
= tree_to_uhwi (TYPE_SIZE_UNIT (TREE_TYPE (TREE_TYPE (src))));
|
||||
|
||||
if (offset_node && TREE_CODE (offset_node) != INTEGER_CST)
|
||||
/* Set MAXELTS to sizeof (SRC) / sizeof (*SRC) - 1, the maximum possible
|
||||
length of SRC. */
|
||||
unsigned maxelts = TREE_STRING_LENGTH (src) / eltsize - 1;
|
||||
|
||||
/* PTR can point to the byte representation of any string type, including
|
||||
char* and wchar_t*. */
|
||||
const char *ptr = TREE_STRING_POINTER (src);
|
||||
|
||||
if (byteoff && TREE_CODE (byteoff) != INTEGER_CST)
|
||||
{
|
||||
/* If the string has an internal zero byte (e.g., "foo\0bar"), we can't
|
||||
compute the offset to the following null if we don't know where to
|
||||
start searching for it. */
|
||||
int i;
|
||||
|
||||
for (i = 0; i < max; i++)
|
||||
if (ptr[i] == 0)
|
||||
if (string_length (ptr, eltsize, maxelts) < maxelts)
|
||||
{
|
||||
/* Return when an embedded null character is found. */
|
||||
return NULL_TREE;
|
||||
}
|
||||
|
||||
/* We don't know the starting offset, but we do know that the string
|
||||
has no internal zero bytes. We can assume that the offset falls
|
||||
|
@ -575,27 +615,31 @@ c_strlen (tree src, int only_value)
|
|||
and return that. This would perhaps not be valid if we were dealing
|
||||
with named arrays in addition to literal string constants. */
|
||||
|
||||
return size_diffop_loc (loc, size_int (max), offset_node);
|
||||
return size_diffop_loc (loc, size_int (maxelts * eltsize), byteoff);
|
||||
}
|
||||
|
||||
/* Offset from the beginning of the string in elements. */
|
||||
HOST_WIDE_INT eltoff;
|
||||
|
||||
/* We have a known offset into the string. Start searching there for
|
||||
a null character if we can represent it as a single HOST_WIDE_INT. */
|
||||
if (offset_node == 0)
|
||||
offset = 0;
|
||||
else if (! tree_fits_shwi_p (offset_node))
|
||||
offset = -1;
|
||||
if (byteoff == 0)
|
||||
eltoff = 0;
|
||||
else if (! tree_fits_shwi_p (byteoff))
|
||||
eltoff = -1;
|
||||
else
|
||||
offset = tree_to_shwi (offset_node);
|
||||
eltoff = tree_to_shwi (byteoff) / eltsize;
|
||||
|
||||
/* If the offset is known to be out of bounds, warn, and call strlen at
|
||||
runtime. */
|
||||
if (offset < 0 || offset > max)
|
||||
if (eltoff < 0 || eltoff > maxelts)
|
||||
{
|
||||
/* Suppress multiple warnings for propagated constant strings. */
|
||||
if (only_value != 2
|
||||
&& !TREE_NO_WARNING (src))
|
||||
{
|
||||
warning_at (loc, 0, "offset outside bounds of constant string");
|
||||
warning_at (loc, 0, "offset %qwi outside bounds of constant string",
|
||||
eltoff);
|
||||
TREE_NO_WARNING (src) = 1;
|
||||
}
|
||||
return NULL_TREE;
|
||||
|
@ -605,9 +649,12 @@ c_strlen (tree src, int only_value)
|
|||
constructed with build_string will have nulls appended, we win even
|
||||
if we get handed something like (char[4])"abcd".
|
||||
|
||||
Since OFFSET is our starting index into the string, no further
|
||||
Since ELTOFF is our starting index into the string, no further
|
||||
calculation is needed. */
|
||||
return ssize_int (strlen (ptr + offset));
|
||||
unsigned len = string_length (ptr + eltoff * eltsize, eltsize,
|
||||
maxelts - eltoff);
|
||||
|
||||
return ssize_int (len);
|
||||
}
|
||||
|
||||
/* Return a constant integer corresponding to target reading
|
||||
|
|
|
@ -79,6 +79,11 @@ along with GCC; see the file COPYING3. If not see
|
|||
#include "substring-locations.h"
|
||||
#include "diagnostic.h"
|
||||
|
||||
/* The likely worst case value of MB_LEN_MAX for the target, large enough
|
||||
for UTF-8. Ideally, this would be obtained by a target hook if it were
|
||||
to be used for optimization but it's good enough as is for warnings. */
|
||||
#define target_mb_len_max 6
|
||||
|
||||
namespace {
|
||||
|
||||
const pass_data pass_data_sprintf_length = {
|
||||
|
@ -150,17 +155,30 @@ struct format_result
|
|||
unsigned HOST_WIDE_INT number_chars_max;
|
||||
|
||||
/* True when the range given by NUMBER_CHARS_MIN and NUMBER_CHARS_MAX
|
||||
is the output of all directives determined to be bounded to some
|
||||
subrange of their types or possible lengths, false otherwise.
|
||||
can be relied on for value range propagation, false otherwise.
|
||||
This means that BOUNDED must not be set if the number of bytes
|
||||
produced by any directive is unspecified or implementation-
|
||||
defined (unless the implementation's behavior is known and
|
||||
determined via a target hook).
|
||||
Note that BOUNDED only implies that the length of a function's
|
||||
output is known to be within some range, not that it's constant
|
||||
and a candidate for folding. */
|
||||
and a candidate for string folding. BOUNDED is a stronger
|
||||
guarantee than KNOWNRANGE. */
|
||||
bool bounded;
|
||||
|
||||
/* True when the range above is obtained from known values of
|
||||
directive arguments or their bounds and not the result of
|
||||
heuristics that depend on warning levels. It is used to
|
||||
issue stricter diagnostics in cases where strings of unknown
|
||||
lengths are bounded by the arrays they are determined to
|
||||
refer to. KNOWNRANGE must not be used to set the range of
|
||||
the return value of a call. */
|
||||
bool knownrange;
|
||||
|
||||
/* True when the output of the formatted call is constant (and
|
||||
thus a candidate for string constant folding). This is rare
|
||||
and typically requires that the arguments of all directives
|
||||
are also constant. Constant implies bounded. */
|
||||
are also constant. CONSTANT implies BOUNDED. */
|
||||
bool constant;
|
||||
|
||||
/* True if no individual directive resulted in more than 4095 bytes
|
||||
|
@ -216,15 +234,31 @@ target_int_min ()
|
|||
return int_min;
|
||||
}
|
||||
|
||||
/* Return the value of INT_MAX for the target. */
|
||||
/* Return the largest value for TYPE on the target. */
|
||||
|
||||
static unsigned HOST_WIDE_INT
|
||||
target_max_value (tree type)
|
||||
{
|
||||
const unsigned HOST_WIDE_INT max_value
|
||||
= HOST_WIDE_INT_M1U >> (HOST_BITS_PER_WIDE_INT
|
||||
- TYPE_PRECISION (type) + 1);
|
||||
return max_value;
|
||||
}
|
||||
|
||||
/* Return the value of INT_MAX for the target. */
|
||||
|
||||
static inline unsigned HOST_WIDE_INT
|
||||
target_int_max ()
|
||||
{
|
||||
const unsigned HOST_WIDE_INT int_max
|
||||
= HOST_WIDE_INT_M1U >> (HOST_BITS_PER_WIDE_INT
|
||||
- TYPE_PRECISION (integer_type_node) + 1);
|
||||
return int_max;
|
||||
return target_max_value (integer_type_node);
|
||||
}
|
||||
|
||||
/* Return the value of SIZE_MAX for the target. */
|
||||
|
||||
static inline unsigned HOST_WIDE_INT
|
||||
target_size_max ()
|
||||
{
|
||||
return target_max_value (size_type_node);
|
||||
}
|
||||
|
||||
/* Return the constant initial value of DECL if available or DECL
|
||||
|
@ -412,6 +446,12 @@ struct result_range
|
|||
|
||||
struct fmtresult
|
||||
{
|
||||
fmtresult ()
|
||||
: argmin (), argmax (), knownrange (), bounded (), constant ()
|
||||
{
|
||||
range.min = range.max = HOST_WIDE_INT_MAX;
|
||||
}
|
||||
|
||||
/* The range a directive's argument is in. */
|
||||
tree argmin, argmax;
|
||||
|
||||
|
@ -419,11 +459,17 @@ struct fmtresult
|
|||
results in on output for an argument in the range above. */
|
||||
result_range range;
|
||||
|
||||
/* True when the range above is obtained from a known value of
|
||||
a directive's argument or its bounds and not the result of
|
||||
heuristics that depend on warning levels. */
|
||||
bool knownrange;
|
||||
|
||||
/* True when the range is the result of an argument determined
|
||||
to be bounded to a subrange of its type or value (such as by
|
||||
value range propagation or the width of the formt directive),
|
||||
false otherwise. */
|
||||
bool bounded;
|
||||
|
||||
/* True when the output of a directive is constant. This is rare
|
||||
and typically requires that the argument(s) of the directive
|
||||
are also constant (such as determined by constant propagation,
|
||||
|
@ -730,7 +776,7 @@ format_integer (const conversion_spec &, tree);
|
|||
static fmtresult
|
||||
format_pointer (const conversion_spec &spec, tree arg)
|
||||
{
|
||||
fmtresult res = fmtresult ();
|
||||
fmtresult res;
|
||||
|
||||
/* Determine the target's integer format corresponding to "%p". */
|
||||
const char *flags;
|
||||
|
@ -869,14 +915,7 @@ format_integer (const conversion_spec &spec, tree arg)
|
|||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
fmtresult res = fmtresult ();
|
||||
res.range.min = HOST_WIDE_INT_MAX;
|
||||
res.range.max = HOST_WIDE_INT_MAX;
|
||||
res.bounded = false;
|
||||
res.constant = false;
|
||||
return res;
|
||||
}
|
||||
return fmtresult ();
|
||||
}
|
||||
|
||||
/* The type of the argument to the directive, either deduced from
|
||||
|
@ -897,12 +936,13 @@ format_integer (const conversion_spec &spec, tree arg)
|
|||
{
|
||||
/* The minimum and maximum number of bytes produced by
|
||||
the directive. */
|
||||
fmtresult res = fmtresult ();
|
||||
fmtresult res;
|
||||
|
||||
/* When a constant argument has been provided use its value
|
||||
rather than type to determine the length of the output. */
|
||||
res.bounded = true;
|
||||
res.constant = true;
|
||||
res.knownrange = true;
|
||||
|
||||
/* Base to format the number in. */
|
||||
int base;
|
||||
|
@ -975,12 +1015,10 @@ format_integer (const conversion_spec &spec, tree arg)
|
|||
/* Don't bother with invalid arguments since they likely would
|
||||
have already been diagnosed, and disable any further checking
|
||||
of the format string by returning [-1, -1]. */
|
||||
fmtresult res = fmtresult ();
|
||||
res.range.min = res.range.max = HOST_WIDE_INT_M1U;
|
||||
return res;
|
||||
return fmtresult ();
|
||||
}
|
||||
|
||||
fmtresult res = fmtresult ();
|
||||
fmtresult res;
|
||||
|
||||
/* Using either the range the non-constant argument is in, or its
|
||||
type (either "formal" or actual), create a range of values that
|
||||
|
@ -1029,9 +1067,10 @@ format_integer (const conversion_spec &spec, tree arg)
|
|||
argmax = res.argmax;
|
||||
}
|
||||
|
||||
/* The argument is bounded by the range of values determined
|
||||
by Value Range Propagation. */
|
||||
/* The argument is bounded by the known range of values
|
||||
determined by Value Range Propagation. */
|
||||
res.bounded = true;
|
||||
res.knownrange = true;
|
||||
}
|
||||
else if (range_type == VR_ANTI_RANGE)
|
||||
{
|
||||
|
@ -1047,6 +1086,12 @@ format_integer (const conversion_spec &spec, tree arg)
|
|||
if (is_gimple_assign (def))
|
||||
{
|
||||
tree_code code = gimple_assign_rhs_code (def);
|
||||
if (code == INTEGER_CST)
|
||||
{
|
||||
arg = gimple_assign_rhs1 (def);
|
||||
return format_integer (spec, arg);
|
||||
}
|
||||
|
||||
if (code == NOP_EXPR)
|
||||
argtype = TREE_TYPE (gimple_assign_rhs1 (def));
|
||||
}
|
||||
|
@ -1111,7 +1156,7 @@ format_integer (const conversion_spec &spec, tree arg)
|
|||
SPEC the largest value in the real floating TYPE. */
|
||||
|
||||
static int
|
||||
format_floating_max (tree type, char spec)
|
||||
format_floating_max (tree type, char spec, int prec = -1)
|
||||
{
|
||||
machine_mode mode = TYPE_MODE (type);
|
||||
|
||||
|
@ -1136,9 +1181,21 @@ format_floating_max (tree type, char spec)
|
|||
mpfr_init2 (x, rfmt->p);
|
||||
mpfr_from_real (x, &rv, GMP_RNDN);
|
||||
|
||||
int n;
|
||||
|
||||
if (-1 < prec)
|
||||
{
|
||||
const char fmt[] = { '%', '.', '*', 'R', spec, '\0' };
|
||||
n = mpfr_snprintf (NULL, 0, fmt, prec, x);
|
||||
}
|
||||
else
|
||||
{
|
||||
const char fmt[] = { '%', 'R', spec, '\0' };
|
||||
int n = mpfr_snprintf (NULL, 0, fmt, x);
|
||||
return n;
|
||||
n = mpfr_snprintf (NULL, 0, fmt, x);
|
||||
}
|
||||
|
||||
/* Return a value one greater to account for the leading minus sign. */
|
||||
return n + 1;
|
||||
}
|
||||
|
||||
/* Return a range representing the minimum and maximum number of bytes
|
||||
|
@ -1170,19 +1227,11 @@ format_floating (const conversion_spec &spec, int width, int prec)
|
|||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
fmtresult res = fmtresult ();
|
||||
res.range.min = HOST_WIDE_INT_MAX;
|
||||
res.range.max = HOST_WIDE_INT_MAX;
|
||||
res.bounded = false;
|
||||
res.constant = false;
|
||||
return res;
|
||||
}
|
||||
return fmtresult ();
|
||||
}
|
||||
|
||||
/* The minimum and maximum number of bytes produced by the directive. */
|
||||
fmtresult res = fmtresult ();
|
||||
res.constant = false;
|
||||
fmtresult res;
|
||||
|
||||
/* Log10 of of the maximum number of exponent digits for the type. */
|
||||
int logexpdigs = 2;
|
||||
|
@ -1206,13 +1255,11 @@ format_floating (const conversion_spec &spec, int width, int prec)
|
|||
{
|
||||
/* The minimum output is "0x.p+0". */
|
||||
res.range.min = 6 + (prec > 0 ? prec : 0);
|
||||
res.range.max = format_floating_max (type, 'a', prec);
|
||||
|
||||
/* Compute the maximum just once. */
|
||||
static const int a_max[] = {
|
||||
format_floating_max (double_type_node, 'a'),
|
||||
format_floating_max (long_double_type_node, 'a')
|
||||
};
|
||||
res.range.max = a_max [ldbl];
|
||||
/* The output of "%a" is fully specified only when precision
|
||||
is explicitly specified. */
|
||||
res.bounded = -1 < prec;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -1229,6 +1276,9 @@ format_floating (const conversion_spec &spec, int width, int prec)
|
|||
included), plus the difference between the minimum exponent
|
||||
of 2 and the maximum exponent for the type. */
|
||||
res.range.max = res.range.min + !sign + logexpdigs - 2;
|
||||
|
||||
/* "%e" is fully specified and the range of bytes is bounded. */
|
||||
res.bounded = true;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -1245,6 +1295,9 @@ format_floating (const conversion_spec &spec, int width, int prec)
|
|||
format_floating_max (long_double_type_node, 'f')
|
||||
};
|
||||
res.range.max = f_max [ldbl];
|
||||
|
||||
/* "%f" is fully specified and the range of bytes is bounded. */
|
||||
res.bounded = true;
|
||||
break;
|
||||
}
|
||||
case 'G':
|
||||
|
@ -1259,18 +1312,14 @@ format_floating (const conversion_spec &spec, int width, int prec)
|
|||
format_floating_max (long_double_type_node, 'g')
|
||||
};
|
||||
res.range.max = g_max [ldbl];
|
||||
|
||||
/* "%g" is fully specified and the range of bytes is bounded. */
|
||||
res.bounded = true;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
fmtresult res = fmtresult ();
|
||||
res.range.min = HOST_WIDE_INT_MAX;
|
||||
res.range.max = HOST_WIDE_INT_MAX;
|
||||
res.bounded = false;
|
||||
res.constant = false;
|
||||
return res;
|
||||
}
|
||||
return fmtresult ();
|
||||
}
|
||||
|
||||
if (width > 0)
|
||||
|
@ -1281,9 +1330,6 @@ format_floating (const conversion_spec &spec, int width, int prec)
|
|||
res.range.max = width;
|
||||
}
|
||||
|
||||
/* The argument is only considered bounded when the range of output
|
||||
bytes is exact. */
|
||||
res.bounded = res.range.min == res.range.max;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -1298,7 +1344,7 @@ format_floating (const conversion_spec &spec, tree arg)
|
|||
int prec = -1;
|
||||
|
||||
/* The minimum and maximum number of bytes produced by the directive. */
|
||||
fmtresult res = fmtresult ();
|
||||
fmtresult res;
|
||||
res.constant = arg && TREE_CODE (arg) == REAL_CST;
|
||||
|
||||
if (spec.have_width)
|
||||
|
@ -1390,7 +1436,16 @@ format_floating (const conversion_spec &spec, tree arg)
|
|||
*minmax[i] = mpfr_snprintf (NULL, 0, fmtstr, mpfrval);
|
||||
}
|
||||
|
||||
res.bounded = res.range.min < target_int_max ();
|
||||
/* The output of all directives except "%a" is fully specified
|
||||
and so the result is bounded unless it exceeds INT_MAX.
|
||||
For "%a" the output is fully specified only when precision
|
||||
is explicitly specified. */
|
||||
res.bounded = ((TOUPPER (spec.specifier) != 'A'
|
||||
|| (0 <= prec && (unsigned) prec < target_int_max ()))
|
||||
&& res.range.min < target_int_max ());
|
||||
|
||||
/* The range of output is known even if the result isn't bounded. */
|
||||
res.knownrange = true;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -1405,14 +1460,7 @@ static fmtresult
|
|||
get_string_length (tree str)
|
||||
{
|
||||
if (!str)
|
||||
{
|
||||
fmtresult res;
|
||||
res.range.min = HOST_WIDE_INT_MAX;
|
||||
res.range.max = HOST_WIDE_INT_MAX;
|
||||
res.bounded = false;
|
||||
res.constant = false;
|
||||
return res;
|
||||
}
|
||||
return fmtresult ();
|
||||
|
||||
if (tree slen = c_strlen (str, 1))
|
||||
{
|
||||
|
@ -1421,6 +1469,7 @@ get_string_length (tree str)
|
|||
res.range.min = res.range.max = tree_to_shwi (slen);
|
||||
res.bounded = true;
|
||||
res.constant = true;
|
||||
res.knownrange = true;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -1434,7 +1483,7 @@ get_string_length (tree str)
|
|||
|
||||
if (lenrange [0] || lenrange [1])
|
||||
{
|
||||
fmtresult res = fmtresult ();
|
||||
fmtresult res;
|
||||
|
||||
res.range.min = (tree_fits_uhwi_p (lenrange[0])
|
||||
? tree_to_uhwi (lenrange[0]) : 1 < warn_format_length);
|
||||
|
@ -1445,11 +1494,13 @@ get_string_length (tree str)
|
|||
by STR are known to be bounded (though not necessarily by their
|
||||
actual length but perhaps by their maximum possible length). */
|
||||
res.bounded = res.range.max < target_int_max ();
|
||||
res.knownrange = res.bounded;
|
||||
|
||||
/* Set RES.CONSTANT to false even though that may be overly
|
||||
conservative in rare cases like: 'x ? a : b' where a and
|
||||
b have the same lengths and consist of the same characters. */
|
||||
res.constant = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -1479,7 +1530,7 @@ format_string (const conversion_spec &spec, tree arg)
|
|||
prec = (TREE_CODE (spec.star_precision) == INTEGER_CST
|
||||
? tree_to_shwi (spec.star_precision) : -1);
|
||||
|
||||
fmtresult res = fmtresult ();
|
||||
fmtresult res;
|
||||
|
||||
/* The maximum number of bytes for an unknown wide character argument
|
||||
to a "%lc" directive adjusted for precision but not field width. */
|
||||
|
@ -1515,13 +1566,16 @@ format_string (const conversion_spec &spec, tree arg)
|
|||
locale, which is unfortunately, unknown. */
|
||||
res.range.min = 1 == warn_format_length ? !nul : nul < 1;
|
||||
res.range.max = max_bytes_for_unknown_wc;
|
||||
res.bounded = true;
|
||||
/* The range above is good enough to issue warnings but not
|
||||
for value range propagation, so clear BOUNDED. */
|
||||
res.bounded = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* A plain '%c' directive. */
|
||||
/* A plain '%c' directive. Its ouput is exactly 1. */
|
||||
res.range.min = res.range.max = 1;
|
||||
res.bounded = true;
|
||||
res.knownrange = true;
|
||||
res.constant = arg && TREE_CODE (arg) == INTEGER_CST;
|
||||
}
|
||||
}
|
||||
|
@ -1533,24 +1587,35 @@ format_string (const conversion_spec &spec, tree arg)
|
|||
{
|
||||
gcc_checking_assert (slen.range.min == slen.range.max);
|
||||
|
||||
res.bounded = true;
|
||||
|
||||
/* A '%s' directive with a string argument with constant length. */
|
||||
res.range = slen.range;
|
||||
|
||||
/* The output of "%s" and "%ls" directives with a constant
|
||||
string is in a known range. For "%s" it is the length
|
||||
of the string. For "%ls" it is in the range [length,
|
||||
length * MB_LEN_MAX]. (The final range can be further
|
||||
constrained by width and precision but it's always known.) */
|
||||
res.knownrange = true;
|
||||
|
||||
if (spec.modifier == FMT_LEN_l)
|
||||
{
|
||||
if (warn_format_length > 2)
|
||||
bounded = false;
|
||||
|
||||
if (warn_format_length > 1)
|
||||
{
|
||||
res.range.min *= 6;
|
||||
/* Leave the minimum number of bytes the wide string
|
||||
converts to equal to its length and set the maximum
|
||||
to the worst case length which is the string length
|
||||
multiplied by MB_LEN_MAX. */
|
||||
|
||||
/* It's possible to be smarter about computing the maximum
|
||||
by scanning the wide string for any 8-bit characters and
|
||||
if it contains none, using its length for the maximum.
|
||||
Even though this would be simple to do it's unlikely to
|
||||
be worth it when dealing with wide characters. */
|
||||
res.range.max *= 6;
|
||||
res.range.max *= target_mb_len_max;
|
||||
}
|
||||
|
||||
/* For a wide character string, use precision as the maximum
|
||||
even if precision is greater than the string length since
|
||||
the number of bytes the string converts to may be greater
|
||||
|
@ -1559,7 +1624,12 @@ format_string (const conversion_spec &spec, tree arg)
|
|||
res.range.max = prec;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* The output od a "%s" directive with a constant argument
|
||||
is bounded, constant, and obviously in a known range. */
|
||||
res.bounded = true;
|
||||
res.constant = true;
|
||||
}
|
||||
|
||||
if (0 <= prec && (unsigned)prec < res.range.min)
|
||||
{
|
||||
|
@ -1577,9 +1647,11 @@ format_string (const conversion_spec &spec, tree arg)
|
|||
|
||||
if (0 <= prec)
|
||||
{
|
||||
if ((unsigned)prec < slen.range.min
|
||||
|| slen.range.min >= target_int_max ())
|
||||
if (slen.range.min >= target_int_max ())
|
||||
slen.range.min = max_bytes_for_unknown_str;
|
||||
else if ((unsigned)prec < slen.range.min)
|
||||
slen.range.min = prec;
|
||||
|
||||
if ((unsigned)prec < slen.range.max
|
||||
|| slen.range.max >= target_int_max ())
|
||||
slen.range.max = prec;
|
||||
|
@ -1597,6 +1669,7 @@ format_string (const conversion_spec &spec, tree arg)
|
|||
specified to limit the number of bytes or when the number
|
||||
of bytes is known or contrained to some range. */
|
||||
res.bounded = 0 <= prec || slen.bounded;
|
||||
res.knownrange = slen.knownrange;
|
||||
res.constant = false;
|
||||
}
|
||||
}
|
||||
|
@ -1613,6 +1686,11 @@ format_string (const conversion_spec &spec, tree arg)
|
|||
&& bounded)
|
||||
res.bounded = true;
|
||||
|
||||
/* When precision is specified the range of characters on output
|
||||
is known to be bounded by it. */
|
||||
if (-1 < prec)
|
||||
res.knownrange = true;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -1655,13 +1733,29 @@ format_directive (const pass_sprintf_length::call_info &info,
|
|||
/* Compute the (approximate) length of the formatted output. */
|
||||
fmtresult fmtres = spec.fmtfunc (spec, arg);
|
||||
|
||||
/* The overall result is bounded only if the output of every
|
||||
directive is exact or bounded. */
|
||||
res->bounded = res->bounded && fmtres.bounded;
|
||||
res->constant = res->constant && fmtres.constant;
|
||||
/* The overall result is bounded and constant only if the output
|
||||
of every directive is bounded and constant, respectively. */
|
||||
res->bounded &= fmtres.bounded;
|
||||
res->constant &= fmtres.constant;
|
||||
|
||||
if (fmtres.range.max >= HOST_WIDE_INT_MAX)
|
||||
/* Record whether the output of all directives is known to be
|
||||
bounded by some maximum, implying that their arguments are
|
||||
either known exactly or determined to be in a known range
|
||||
or, for strings, limited by the upper bounds of the arrays
|
||||
they refer to. */
|
||||
res->knownrange &= fmtres.knownrange;
|
||||
|
||||
if (!fmtres.knownrange)
|
||||
{
|
||||
/* Only when the range is known, check it against the host value
|
||||
of INT_MAX. Otherwise the range doesn't correspond to known
|
||||
values of the argument. */
|
||||
if (fmtres.range.max >= target_int_max ())
|
||||
{
|
||||
/* Normalize the MAX counter to avoid having to deal with it
|
||||
later. The counter can be less than HOST_WIDE_INT_M1U
|
||||
when compiling for an ILP32 target on an LP64 host. */
|
||||
fmtres.range.max = HOST_WIDE_INT_M1U;
|
||||
/* Disable exact and maximum length checking after a failure
|
||||
to determine the maximum number of characters (for example
|
||||
for wide characters or wide character strings) but continue
|
||||
|
@ -1670,7 +1764,7 @@ format_directive (const pass_sprintf_length::call_info &info,
|
|||
res->number_chars = HOST_WIDE_INT_M1U;
|
||||
}
|
||||
|
||||
if (fmtres.range.min >= HOST_WIDE_INT_MAX)
|
||||
if (fmtres.range.min >= target_int_max ())
|
||||
{
|
||||
/* Disable exact length checking after a failure to determine
|
||||
even the minimum number of characters (it shouldn't happen
|
||||
|
@ -1679,6 +1773,7 @@ format_directive (const pass_sprintf_length::call_info &info,
|
|||
res->number_chars = HOST_WIDE_INT_M1U;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compute the number of available bytes in the destination. There
|
||||
must always be at least one byte of space for the terminating
|
||||
|
@ -1725,10 +1820,14 @@ format_directive (const pass_sprintf_length::call_info &info,
|
|||
}
|
||||
}
|
||||
else if (navail < fmtres.range.max
|
||||
&& (fmtres.bounded || 1 < warn_format_length))
|
||||
&& (((spec.specifier == 's'
|
||||
&& fmtres.range.max < HOST_WIDE_INT_MAX)
|
||||
/* && (spec.precision || spec.star_precision) */)
|
||||
|| 1 < warn_format_length))
|
||||
{
|
||||
/* The maximum directive output is longer than there is
|
||||
room in the destination and the output is either bounded
|
||||
room in the destination and the output length is either
|
||||
explicitly constrained by the precision (for strings)
|
||||
or the warning level is greater than 1. */
|
||||
if (fmtres.range.max >= HOST_WIDE_INT_MAX)
|
||||
{
|
||||
|
@ -1910,10 +2009,13 @@ add_bytes (const pass_sprintf_length::call_info &info,
|
|||
|
||||
/* If issuing a diagnostic (only when one hasn't already been issued),
|
||||
distinguish between a possible overflow ("may write") and a certain
|
||||
overflow somewhere "past the end." (Ditto for truncation.) */
|
||||
overflow somewhere "past the end." (Ditto for truncation.)
|
||||
KNOWNRANGE is used to warn even at level 1 about possibly writing
|
||||
past the end or truncation due to strings of unknown lengths that
|
||||
are bounded by the arrays they are known to refer to. */
|
||||
if (!res->warned
|
||||
&& (avail_range.max < nbytes
|
||||
|| ((res->bounded || 1 < warn_format_length)
|
||||
|| ((res->knownrange || 1 < warn_format_length)
|
||||
&& avail_range.min < nbytes)))
|
||||
{
|
||||
/* Set NAVAIL to the number of available bytes used to decide
|
||||
|
@ -1921,7 +2023,7 @@ add_bytes (const pass_sprintf_length::call_info &info,
|
|||
warning will depend on AVAIL_RANGE. */
|
||||
unsigned HOST_WIDE_INT navail = avail_range.max;
|
||||
if (nbytes <= navail && avail_range.min < HOST_WIDE_INT_MAX
|
||||
&& (res->bounded || 1 < warn_format_length))
|
||||
&& (res->knownrange || 1 < warn_format_length))
|
||||
navail = avail_range.min;
|
||||
|
||||
/* Compute the offset of the first format character that is beyond
|
||||
|
@ -1932,8 +2034,12 @@ add_bytes (const pass_sprintf_length::call_info &info,
|
|||
|
||||
size_t len = strlen (info.fmtstr + off);
|
||||
|
||||
/* Create a location that underscores the substring of the format
|
||||
string that is or may be written past the end (or is or may be
|
||||
truncated), pointing the caret at the first character of the
|
||||
substring. */
|
||||
substring_loc loc
|
||||
(info.fmtloc, TREE_TYPE (info.format), off - !len, len ? off : 0,
|
||||
(info.fmtloc, TREE_TYPE (info.format), off, len ? off : 0,
|
||||
off + len - !!len);
|
||||
|
||||
/* Is the output of the last directive the result of the argument
|
||||
|
@ -1944,7 +2050,7 @@ add_bytes (const pass_sprintf_length::call_info &info,
|
|||
= (res->number_chars_min < res->number_chars_max
|
||||
&& res->number_chars_min < info.objsize);
|
||||
|
||||
if (!end && (nbytes - navail) == 1)
|
||||
if (!end && ((nbytes - navail) == 1 || boundrange))
|
||||
{
|
||||
/* There is room for the rest of the format string but none
|
||||
for the terminating nul. */
|
||||
|
@ -2082,10 +2188,11 @@ pass_sprintf_length::compute_format_length (const call_info &info,
|
|||
/* Reset exact, minimum, and maximum character counters. */
|
||||
res->number_chars = res->number_chars_min = res->number_chars_max = 0;
|
||||
|
||||
/* No directive has been seen yet so the output is bounded and constant
|
||||
(with no conversion producing more than 4K bytes) until determined
|
||||
otherwise. */
|
||||
/* No directive has been seen yet so the length of output is bounded
|
||||
by the known range [0, 0] and constant (with no conversion producing
|
||||
more than 4K bytes) until determined otherwise. */
|
||||
res->bounded = true;
|
||||
res->knownrange = true;
|
||||
res->constant = true;
|
||||
res->under4k = true;
|
||||
res->floating = false;
|
||||
|
@ -2591,10 +2698,10 @@ pass_sprintf_length::handle_gimple_call (gimple_stmt_iterator gsi)
|
|||
if (TREE_CODE (size) == INTEGER_CST)
|
||||
{
|
||||
dstsize = tree_to_uhwi (size);
|
||||
/* No object can be larger than HOST_WIDE_INT_MAX bytes
|
||||
(half the address space). This imposes a limit that's
|
||||
one byte less than that. */
|
||||
if (dstsize >= HOST_WIDE_INT_MAX)
|
||||
/* No object can be larger than SIZE_MAX bytes (half the address
|
||||
space) on the target. This imposes a limit that's one byte
|
||||
less than that. */
|
||||
if (dstsize >= target_size_max () / 2)
|
||||
warning_at (gimple_location (info.callstmt), OPT_Wformat_length_,
|
||||
"specified destination size %wu too large",
|
||||
dstsize);
|
||||
|
@ -2640,7 +2747,7 @@ pass_sprintf_length::handle_gimple_call (gimple_stmt_iterator gsi)
|
|||
info.objsize = dstsize < objsize ? dstsize : objsize;
|
||||
|
||||
if (info.bounded
|
||||
&& dstsize != HOST_WIDE_INT_M1U && objsize < dstsize)
|
||||
&& dstsize < target_size_max () / 2 && objsize < dstsize)
|
||||
{
|
||||
warning_at (gimple_location (info.callstmt), OPT_Wformat_length_,
|
||||
"specified size %wu exceeds the size %wu "
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
2016-10-24 Martin Sebor <msebor@redhat.com>
|
||||
|
||||
PR middle-end/77735
|
||||
* gcc.dg/tree-ssa/builtin-sprintf-2.c: Add test cases.
|
||||
* gcc.dg/tree-ssa/builtin-sprintf-warn-1.c: Same.
|
||||
* gcc.dg/tree-ssa/builtin-sprintf-warn-2.c: Same.
|
||||
* gcc.dg/tree-ssa/builtin-sprintf-warn-3.c: Adjust/relax.
|
||||
* gcc.dg/tree-ssa/builtin-sprintf-warn-4.c: Add test cases.
|
||||
* gcc.dg/tree-ssa/builtin-sprintf-warn-6.c: XFAIL for LP64 only.
|
||||
* gcc.dg/tree-ssa/builtin-sprintf.c: Add test cases.
|
||||
|
||||
2016-10-24 Richard Biener <rguenther@suse.de>
|
||||
|
||||
PR testsuite/71491
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* Test to verify that the return value of calls to __builtin_sprintf
|
||||
is not folded if the call has undefined behavior even if it would
|
||||
is not folded if the call isn't fully specified, even if it would
|
||||
otherwise produce a known number of bytes on output, and that if
|
||||
the return value is in a known range the range is not made
|
||||
available to subsequent passes and doesn't affect branching and
|
||||
|
@ -22,7 +22,8 @@ char buf8k [8192];
|
|||
#define CAT(a, b) concat (a, b)
|
||||
|
||||
#define EQL(expect, size, fmt, ...) \
|
||||
void CAT (test_on_line_, __LINE__)(void) \
|
||||
void __attribute__ ((noinline, noclone)) \
|
||||
CAT (test_on_line_, __LINE__)(void) \
|
||||
{ \
|
||||
if (!LINE || LINE == __LINE__) \
|
||||
{ \
|
||||
|
@ -37,7 +38,8 @@ char buf8k [8192];
|
|||
to the formatted function is not treated as a constant or made available
|
||||
to subsequent optimization passes. */
|
||||
#define RNG(min, max, size, fmt, ...) \
|
||||
void CAT (test_on_line_, __LINE__)(void) \
|
||||
void __attribute__ ((noinline, noclone)) \
|
||||
CAT (test_on_line_, __LINE__)(void) \
|
||||
{ \
|
||||
if (!LINE || LINE == __LINE__) \
|
||||
{ \
|
||||
|
@ -52,6 +54,9 @@ extern int i;
|
|||
extern long li;
|
||||
extern char *str;
|
||||
|
||||
extern double d;
|
||||
extern long double ld;
|
||||
|
||||
/* Verify that overflowing the destination object disables the return
|
||||
value optimization. */
|
||||
EQL (0, 0, "%c", ' ');
|
||||
|
@ -78,7 +83,15 @@ enum { imax2 = (INT_MAX / 2) * 2 };
|
|||
EQL (imax2, -1, "%*c%*c", INT_MAX / 2, 'x', INT_MAX / 2, 'y');
|
||||
|
||||
/* Verify that range information for calls that overflow the destination
|
||||
isn't available. */
|
||||
isn't available.
|
||||
|
||||
+-- lower bound of the tested range
|
||||
| +-- upper bound of the tested range
|
||||
| | +-- size of destination buffer
|
||||
| | | +-- format string
|
||||
| | | | +-- argument(s)
|
||||
| | | | |
|
||||
V V V V V */
|
||||
RNG (0, 0, 0, "%hhi", i)
|
||||
RNG (0, 0, 1, "%hhi", i)
|
||||
RNG (0, 1, 1, "%hhi", i)
|
||||
|
@ -190,11 +203,36 @@ RNG (0, 10, 10, "%i", i)
|
|||
|
||||
#endif
|
||||
|
||||
/* Verify that the output of a "%a" directive with no precision is not
|
||||
considered constant or within a known range (the number of digits
|
||||
after the decimal point is unspecified in this case). The hardcoded
|
||||
ranges correspond to Glibc values. */
|
||||
RNG (6, 6, 7, "%a", 0.0) /* Glibc output: "0x0p+0" */
|
||||
RNG (6, 6, 7, "%a", d)
|
||||
RNG (6, 6, 7, "%.4096a", d)
|
||||
|
||||
RNG (6, 6, 7, "%La", 0.0L) /* Glibc output: "0x0p+0" */
|
||||
RNG (6, 6, 7, "%La", ld)
|
||||
RNG (6, 6, 7, "%.4096La", ld)
|
||||
|
||||
/* Verify that the result of formatting an unknown string isn't optimized
|
||||
into a non-negative range. The string could be longer that 4,095 bytes,
|
||||
resulting in the formatting function having undefined behavior (and
|
||||
returning a negative value as Glibc can for some directives). */
|
||||
RNG (0, INT_MAX, -1, "%-s", str);
|
||||
|
||||
/* Verify the result of a conditional expression involving a string
|
||||
literal and an unknown string isn't optimized. */
|
||||
RNG (0, 1, 4, "%-s", i ? str : "123");
|
||||
RNG (0, 1, 4, "%-s", i ? "123" : str);
|
||||
|
||||
/* Verfy that the output involving wide strings is not optimized
|
||||
(the output is actually bounded by a function of MB_LEN_MAX
|
||||
which should be at least 6 to accommodate UTF-8 but this isn't
|
||||
implemented yet). */
|
||||
RNG (0, 5, 7, "%ls", L"1");
|
||||
RNG (0, 6, 8, "%s%ls", "1", L"2");
|
||||
|
||||
/* Verify that no call to abort has been eliminated and that each call
|
||||
is at the beginning of a basic block (and thus the result of a branch).
|
||||
This latter test tries to verify that the test preceding the call to
|
||||
|
@ -214,5 +252,5 @@ RNG (0, 1, 4, "%-s", i ? "123" : str);
|
|||
|
||||
*/
|
||||
|
||||
/* { dg-final { scan-tree-dump-times ">:\n *__builtin_abort" 105 "optimized" { target { ilp32 || lp64 } } } } */
|
||||
/* { dg-final { scan-tree-dump-times ">:\n *__builtin_abort" 74 "optimized" { target { { ! ilp32 } && { ! lp64 } } } } } */
|
||||
/* { dg-final { scan-tree-dump-times ">:\n *__builtin_abort" 114 "optimized" { target { ilp32 || lp64 } } } } */
|
||||
/* { dg-final { scan-tree-dump-times ">:\n *__builtin_abort" 83 "optimized" { target { { ! ilp32 } && { ! lp64 } } } } } */
|
||||
|
|
|
@ -37,6 +37,18 @@ typedef __WINT_TYPE__ wint_t;
|
|||
|
||||
typedef unsigned char UChar;
|
||||
|
||||
/* Constants used to verify the pass can determine their values even
|
||||
without optimization. */
|
||||
const int cst0 = 0;
|
||||
const int cst1 = 1;
|
||||
const int cst10 = 10;
|
||||
|
||||
/* Initialized global variables used to verify that the pass doesn't
|
||||
use their initial values (they could be modified by calls to other
|
||||
functions). */
|
||||
int var0 = 0;
|
||||
int var10 = 10;
|
||||
|
||||
const char s0[] = "";
|
||||
const char s1[] = "1";
|
||||
const char s2[] = "12";
|
||||
|
@ -372,6 +384,13 @@ void test_sprintf_chk_s_const (void)
|
|||
T (3, "%.0ls", L"1");
|
||||
T (3, "%.1ls", L"1");
|
||||
T (3, "%.2ls", L"1");
|
||||
T (3, "%ls", L"12");
|
||||
|
||||
T (3, "%ls", L"123"); /* { dg-warning "nul past the end" } */
|
||||
T (3, "%.0ls", L"123");
|
||||
T (3, "%.1ls", L"123");
|
||||
T (3, "%.2ls", L"123");
|
||||
T (3, "%.3ls", L"123"); /* { dg-warning "nul past the end" } */
|
||||
}
|
||||
|
||||
/* Exercise the "%hhd", "%hhi", "%hho", "%hhu", and "%hhx" directives
|
||||
|
@ -382,7 +401,9 @@ void test_sprintf_chk_hh_const (void)
|
|||
T (-1, "%hhd", 0);
|
||||
|
||||
T (1, "%hhd", 0); /* { dg-warning "nul past the end" } */
|
||||
T (1, "%hhd", cst0); /* { dg-warning "nul past the end" } */
|
||||
T (1, "%hhd", 1); /* { dg-warning "nul past the end" } */
|
||||
T (1, "%hhd", cst1); /* { dg-warning "nul past the end" } */
|
||||
T (1, "%hhd", -1); /* { dg-warning "into a region" } */
|
||||
T (1, "%+hhd", 0); /* { dg-warning "into a region" } */
|
||||
T (1, "%+hhd", 1); /* { dg-warning "into a region" } */
|
||||
|
@ -402,6 +423,7 @@ void test_sprintf_chk_hh_const (void)
|
|||
T (2, "%+hhi", 9); /* { dg-warning "nul past the end" } */
|
||||
T (2, "%-hhi", 9);
|
||||
T (2, "%hhi", 10); /* { dg-warning "nul past the end" } */
|
||||
T (2, "%hhi", cst10); /* { dg-warning "nul past the end" } */
|
||||
T (2, "%hhi", -1); /* { dg-warning "nul past the end" } */
|
||||
T (2, "% hhi", -1); /* { dg-warning "nul past the end" } */
|
||||
T (2, "%+hhi", -1); /* { dg-warning "nul past the end" } */
|
||||
|
@ -863,6 +885,35 @@ void test_sprintf_chk_z_const (void)
|
|||
T ( 2, "%zu", (size_t)10); /* { dg-warning "nul past the end" } */
|
||||
}
|
||||
|
||||
void test_sprintf_chk_a_const (void)
|
||||
{
|
||||
T (-1, "%a", 0.0);
|
||||
T (-1, "%la", 0.0);
|
||||
|
||||
/* The least number of bytes on output is 6 for "0x0p+0". When precision
|
||||
is missing the number of digits after the decimal point isn't fully
|
||||
specified by C (it seems like a defect). */
|
||||
T (0, "%a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (0, "%la", 0.0); /* { dg-warning "into a region" } */
|
||||
T (1, "%a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (2, "%a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (3, "%a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (4, "%a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (5, "%a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (6, "%a", 0.0); /* { dg-warning "writing a terminating nul" } */
|
||||
T (7, "%a", 0.0);
|
||||
|
||||
T (0, "%.0a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (0, "%.0la", 0.0); /* { dg-warning "into a region" } */
|
||||
T (1, "%.0a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (2, "%.0a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (3, "%.0a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (4, "%.0a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (5, "%.0a", 0.0); /* { dg-warning "into a region" } */
|
||||
T (6, "%.0a", 0.0); /* { dg-warning "writing a terminating nul" } */
|
||||
T (7, "%.0a", 0.0);
|
||||
}
|
||||
|
||||
void test_sprintf_chk_e_const (void)
|
||||
{
|
||||
T (-1, "%E", 0.0);
|
||||
|
@ -893,20 +944,23 @@ void test_sprintf_chk_e_const (void)
|
|||
T ( 6, "%.0e", 1.0);
|
||||
|
||||
/* The actual output of the following directives depends on the rounding
|
||||
mode. Verify that the warning correctly reflects that. */
|
||||
T (12, "%e", 9.999999e+99); /* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%e", 9.9999994e+99); /* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%e", 9.9999995e+99); /* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%e", 9.9999996e+99); /* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%e", 9.9999997e+99); /* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%e", 9.9999998e+99); /* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
mode. Verify that the warning correctly reflects that. At level 1,
|
||||
since the minimum number of bytes output by the directive fits the
|
||||
space the directive itself isn't diagnosed but the terminating nul
|
||||
is. The directive is diagnosed at level 2. */
|
||||
T (12, "%e", 9.999999e+99); /* { dg-warning "terminating nul" } */
|
||||
T (12, "%e", 9.9999994e+99); /* { dg-warning "terminating nul" } */
|
||||
T (12, "%e", 9.9999995e+99); /* { dg-warning "terminating nul" } */
|
||||
T (12, "%e", 9.9999996e+99); /* { dg-warning "terminating nul" } */
|
||||
T (12, "%e", 9.9999997e+99); /* { dg-warning "terminating nul" } */
|
||||
T (12, "%e", 9.9999998e+99); /* { dg-warning "terminating nul" } */
|
||||
|
||||
T (12, "%Le", 9.9999994e+99L);/* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%Le", 9.9999995e+99L);/* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%Le", 9.9999996e+99L);/* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%Le", 9.9999997e+99L);/* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%Le", 9.9999998e+99L);/* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%Le", 9.9999999e+99L);/* { dg-warning "directive writing between 12 and 13 bytes" } */
|
||||
T (12, "%Le", 9.9999994e+99L);/* { dg-warning "terminating nul" } */
|
||||
T (12, "%Le", 9.9999995e+99L);/* { dg-warning "terminating nul" } */
|
||||
T (12, "%Le", 9.9999996e+99L);/* { dg-warning "terminating nul" } */
|
||||
T (12, "%Le", 9.9999997e+99L);/* { dg-warning "terminating nul" } */
|
||||
T (12, "%Le", 9.9999998e+99L);/* { dg-warning "terminating nul" } */
|
||||
T (12, "%Le", 9.9999999e+99L);/* { dg-warning "terminating nul" } */
|
||||
}
|
||||
|
||||
/* At -Wformat-length level 1 unknown numbers are assumed to have
|
||||
|
@ -936,12 +990,14 @@ void test_sprintf_chk_hh_nonconst (int a)
|
|||
{
|
||||
T (-1, "%hhd", a);
|
||||
|
||||
T (0, "%hhd", a); /* { dg-warning "into a region" } */
|
||||
T (0, "%hhd", a); /* { dg-warning ".%hhd. directive writing between 1 and . bytes into a region of size 0" } */
|
||||
T (0, "%hhi", var0); /* { dg-warning "into a region" } */
|
||||
T (0, "%hhi", a); /* { dg-warning "into a region" } */
|
||||
T (0, "%hhu", a); /* { dg-warning "into a region" } */
|
||||
T (0, "%hhx", a); /* { dg-warning "into a region" } */
|
||||
|
||||
T (1, "%hhd", a); /* { dg-warning "nul past the end" } */
|
||||
T (1, "%hhd", var0); /* { dg-warning "nul past the end" } */
|
||||
T (1, "%hhi", a); /* { dg-warning "nul past the end" } */
|
||||
T (1, "%hhu", a); /* { dg-warning "nul past the end" } */
|
||||
T (1, "%hhx", a); /* { dg-warning "nul past the end" } */
|
||||
|
@ -954,19 +1010,23 @@ void test_sprintf_chk_hh_nonconst (int a)
|
|||
T (1, "%-hhi", a); /* { dg-warning "nul past the end" } */
|
||||
|
||||
T (2, "%hhd", a);
|
||||
T (2, "%hhd", var0);
|
||||
T (2, "%hhd", var10);
|
||||
T (2, "%hhi", a);
|
||||
T (2, "%hho", a);
|
||||
T (2, "%hhu", a);
|
||||
T (2, "%hhx", a);
|
||||
|
||||
T (2, "% hhd", a); /* { dg-warning "nul past the end" } */
|
||||
T (2, "% hhd", var0); /* { dg-warning "nul past the end" } */
|
||||
T (2, "% hhd", var10); /* { dg-warning "nul past the end" } */
|
||||
T (2, "% hhi", a); /* { dg-warning "nul past the end" } */
|
||||
T (2, "% hho", a); /* { dg-warning ". . flag used with .%o." } */
|
||||
T (2, "% hhu", a); /* { dg-warning ". . flag used with .%u." } */
|
||||
T (2, "% hhx", a); /* { dg-warning ". . flag used with .%x." } */
|
||||
|
||||
T (2, "#%hho", a); /* { dg-warning "nul past the end" } */
|
||||
T (2, "#%hhx", a); /* { dg-warning "nul past the end" } */
|
||||
T (2, "%#hho", a); /* { dg-warning "nul past the end" } */
|
||||
T (2, "%#hhx", a); /* { dg-warning ".%#hhx. directive writing between 3 and . bytes into a region of size 2" } */
|
||||
|
||||
T (3, "%2hhd", a);
|
||||
T (3, "%2hhi", a);
|
||||
|
@ -1086,8 +1146,7 @@ void test_sprintf_chk_e_nonconst (double d)
|
|||
T ( 1, "%e", d); /* { dg-warning "into a region" } */
|
||||
T ( 2, "%e", d); /* { dg-warning "into a region" } */
|
||||
T ( 3, "%e", d); /* { dg-warning "into a region" } */
|
||||
T (12, "%e", d); /* { dg-warning "past the end" } */
|
||||
T (12, "%e", d); /* { dg-warning "past the end" } */
|
||||
T (12, "%e", d); /* { dg-warning "nul past the end" } */
|
||||
T (13, "%E", d); /* 1.000000E+00 */
|
||||
T (13, "%e", d);
|
||||
T (14, "%E", d);
|
||||
|
|
|
@ -47,10 +47,13 @@ void test_s_const (void)
|
|||
T (1, "%*ls", 0, L"\0");
|
||||
T (1, "%*ls", 1, L""); /* { dg-warning "nul past the end" } */
|
||||
|
||||
T (1, "%ls", L"1"); /* { dg-warning "nul past the end" } */
|
||||
T (1, "%ls", L"1"); /* { dg-warning "directive writing between 1 and 6 bytes into a region of size 1" } */
|
||||
T (1, "%.0ls", L"1");
|
||||
T (2, "%.0ls", L"1");
|
||||
T (2, "%.1ls", L"1");
|
||||
T (2, "%.2ls", L"1"); /* { dg-warning "nul past the end" } */
|
||||
T (2, "%.3ls", L"1"); /* { dg-warning "directive writing between 1 and 3 bytes into a region of size 2" } */
|
||||
T (2, "%.2ls", L"12"); /* { dg-warning "nul past the end" } */
|
||||
|
||||
/* The "%.2ls" directive below will write at a minimum 1 byte (because
|
||||
L"1" is known and can be assumed to convert to at least one multibyte
|
||||
|
@ -64,6 +67,12 @@ void test_s_const (void)
|
|||
T (2, "%.0ls", L"1");
|
||||
T (2, "%.1ls", L"1");
|
||||
T (3, "%.2ls", L"1");
|
||||
T (3, "%.2ls", L"12");
|
||||
T (3, "%.3ls", L"12"); /* { dg-warning "nul past the end" } */
|
||||
T (4, "%.3ls", L"123");
|
||||
T (4, "%.4ls", L"123"); /* { dg-warning "nul past the end" } */
|
||||
T (4, "%.5ls", L"123"); /* { dg-warning "directive writing between 3 and 5 bytes into a region of size 4" } */
|
||||
T (4, "%.6ls", L"123"); /* { dg-warning "directive writing between 3 and 6 bytes into a region of size 4" } */
|
||||
}
|
||||
|
||||
|
||||
|
@ -86,6 +95,8 @@ void test_s_nonconst (const char *s, const wchar_t *ws, struct Arrays *a)
|
|||
T (1, "%.0s", s);
|
||||
T (1, "%.1s", s); /* { dg-warning "writing a terminating nul" } */
|
||||
|
||||
T (1, "%.0ls", ws);
|
||||
T (1, "%.1ls", ws); /* { dg-warning "writing a terminating nul" } */
|
||||
T (1, "%ls", ws); /* { dg-warning "writing a terminating nul" } */
|
||||
|
||||
/* Verify that the size of the array is used in lieu of its length.
|
||||
|
|
|
@ -175,9 +175,9 @@ void test_sprintf_chk_range_schar (signed char *a)
|
|||
T ( 3, "%i", R ( 0, 100)); /* { dg-warning "may write a terminating nul past the end of the destination" } */
|
||||
|
||||
/* The following call may write as few as 3 bytes and as many as 5.
|
||||
It's judgment call how best to diagnose it to make the potential
|
||||
It's a judgment call how best to diagnose it to make the potential
|
||||
problem clear. */
|
||||
T ( 3, "%i%i", R (1, 10), R (9, 10)); /* { dg-warning ".%i. directive writing between 1 and 2 bytes into a region of size 1" } */
|
||||
T ( 3, "%i%i", R (1, 10), R (9, 10)); /* { dg-warning "may write a terminating nul past the end|.%i. directive writing between 1 and 2 bytes into a region of size 1" } */
|
||||
|
||||
T ( 4, "%i%i", R (10, 11), R (12, 13)); /* { dg-warning "nul past the end" } */
|
||||
|
||||
|
@ -187,7 +187,7 @@ void test_sprintf_chk_range_schar (signed char *a)
|
|||
T ( 6, "%i_%i_%i", R (0, 9), R (0, 9), R (0, 10)); /* { dg-warning "may write a terminating nul past the end" } */
|
||||
T ( 6, "%i_%i_%i", R (0, 9), R (0, 10), R (0, 9)); /* { dg-warning "may write a terminating nul past the end" } */
|
||||
T ( 6, "%i_%i_%i", R (0, 10), R (0, 9), R (0, 9)); /* { dg-warning "may write a terminating nul past the end" } */
|
||||
T ( 6, "%i_%i_%i", R (0, 9), R (0, 10), R (0, 10)); /* { dg-warning ".%i. directive writing between 1 and 2 bytes into a region of size 1" } */
|
||||
T ( 6, "%i_%i_%i", R (0, 9), R (0, 10), R (0, 10)); /* { dg-warning "may write a terminating nul past the end|.%i. directive writing between 1 and 2 bytes into a region of size 1" } */
|
||||
}
|
||||
|
||||
void test_sprintf_chk_range_uchar (unsigned char *a, unsigned char *b)
|
||||
|
|
|
@ -3,31 +3,91 @@
|
|||
|
||||
extern int sprintf (char*, const char*, ...);
|
||||
|
||||
char dst [8];
|
||||
char dst [3];
|
||||
|
||||
void test (void)
|
||||
{
|
||||
sprintf (dst + 7, "%-s", "1");
|
||||
/* { dg-warning "writing a terminating nul past the end of the destination" "" { target *-*-* } 10 }
|
||||
{ dg-message "format output 2 bytes into a destination of size 1" "" { target *-*-* } 10 }
|
||||
{ dg-begin-multiline-output "" }
|
||||
sprintf (dst + 7, "%-s", "1");
|
||||
~~^~
|
||||
/* Verify thet the caret points to the (invisible) nul character
|
||||
at the end of the format string (i.e., its closing quote).
|
||||
The redundant argument is there to get around GCC bug 77799. */
|
||||
sprintf (dst + 2, "1", 0);
|
||||
/* { dg-warning "writing a terminating nul past the end of the destination" "nul warning" { target *-*-* } .-1 }
|
||||
{ dg-message "format output 2 bytes into a destination of size 1" "note" { target *-*-* } .-2 }
|
||||
{ dg-begin-multiline-output "-Wformat output: redundant argument" }
|
||||
sprintf (dst + 2, "1", 0);
|
||||
^~~
|
||||
{ dg-end-multiline-output "" }
|
||||
{ dg-begin-multiline-output "" }
|
||||
sprintf (dst + 7, "%-s", "1");
|
||||
{ dg-begin-multiline-output "-Wformat-length output" }
|
||||
sprintf (dst + 2, "1", 0);
|
||||
~^
|
||||
{ dg-end-multiline-output "" }
|
||||
{ dg-begin-multiline-output "note" }
|
||||
sprintf (dst + 2, "1", 0);
|
||||
^~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
{ dg-end-multiline-output "" } */
|
||||
|
||||
/* Verify thet the caret points at the first format character written
|
||||
past the end of the destination. */
|
||||
sprintf (dst, "1234", 0);
|
||||
/* { dg-warning "writing format character .4. at offset 3 past the end of the destination" "nul warning" { target *-*-* } .-1 }
|
||||
{ dg-message "format output 5 bytes into a destination of size 3" "note" { target *-*-* } .-2 }
|
||||
{ dg-begin-multiline-output "-Wformat output: redundant argument" }
|
||||
sprintf (dst, "1234", 0);
|
||||
^~~~~~
|
||||
{ dg-end-multiline-output "" }
|
||||
{ dg-begin-multiline-output "-Wformat-length output" }
|
||||
sprintf (dst, "1234", 0);
|
||||
^
|
||||
{ dg-end-multiline-output "" }
|
||||
{ dg-begin-multiline-output "note" }
|
||||
sprintf (dst, "1234", 0);
|
||||
^~~~~~~~~~~~~~~~~~~~~~~~
|
||||
{ dg-end-multiline-output "" } */
|
||||
|
||||
/* Verify thet the caret points at the first format character written
|
||||
past the end of the destination and the rest of the format string
|
||||
is underlined. */
|
||||
sprintf (dst, "12345", 0);
|
||||
/* { dg-warning "writing format character .4. at offset 3 past the end of the destination" "nul warning" { target *-*-* } .-1 }
|
||||
{ dg-message "format output 6 bytes into a destination of size 3" "note" { target *-*-* } .-2 }
|
||||
{ dg-begin-multiline-output "-Wformat output: redundant argument" }
|
||||
sprintf (dst, "12345", 0);
|
||||
^~~~~~~
|
||||
{ dg-end-multiline-output "" }
|
||||
{ dg-begin-multiline-output "-Wformat-length output" }
|
||||
sprintf (dst, "12345", 0);
|
||||
^~
|
||||
{ dg-end-multiline-output "" }
|
||||
{ dg-begin-multiline-output "note" }
|
||||
sprintf (dst, "12345", 0);
|
||||
^~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
{ dg-end-multiline-output "" } */
|
||||
|
||||
/* Same as above but with a directive. The minus flag is used to
|
||||
get around GCC bug 77671. */
|
||||
sprintf (dst + 2, "%-s", "1");
|
||||
/* { dg-warning "writing a terminating nul past the end of the destination" "warning" { target *-*-* } .-1 }
|
||||
{ dg-message "format output 2 bytes into a destination of size 1" "note" { target *-*-* } .-2 }
|
||||
{ dg-begin-multiline-output "-Wformat-length output" }
|
||||
sprintf (dst + 2, "%-s", "1");
|
||||
~~~^
|
||||
{ dg-end-multiline-output "" }
|
||||
{ dg-begin-multiline-output "note" }
|
||||
sprintf (dst + 2, "%-s", "1");
|
||||
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
{ dg-end-multiline-output "" } */
|
||||
|
||||
sprintf (dst + 7, "%-s", "abcd");
|
||||
/* { dg-warning ".%-s. directive writing 4 bytes into a region of size 1" "" { target *-*-* } 22 }
|
||||
{ dg-message "format output 5 bytes into a destination of size 1" "" { target *-*-* } 22 }
|
||||
{ dg-begin-multiline-output "" }
|
||||
sprintf (dst + 7, "%-s", "abcd");
|
||||
sprintf (dst + 2, "%-s", "abcd");
|
||||
/* { dg-warning ".%-s. directive writing 4 bytes into a region of size 1" "warning" { target *-*-* } .-1 }
|
||||
{ dg-message "format output 5 bytes into a destination of size 1" "note" { target *-*-* } .-2 }
|
||||
{ dg-begin-multiline-output "-Wformat-length output" }
|
||||
sprintf (dst + 2, "%-s", "abcd");
|
||||
^~~ ~~~~~~
|
||||
{ dg-end-multiline-output "" }
|
||||
{ dg-begin-multiline-output "" }
|
||||
sprintf (dst + 7, "%-s", "abcd");
|
||||
{ dg-begin-multiline-output "note" }
|
||||
sprintf (dst + 2, "%-s", "abcd");
|
||||
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
{ dg-end-multiline-output "" } */
|
||||
}
|
||||
|
||||
/* { dg-prune-output "too many arguments for format" } */
|
||||
|
|
|
@ -88,5 +88,5 @@ void fllong (long j, char *p)
|
|||
if (k > 999)
|
||||
return;
|
||||
|
||||
snprintf (p, 4, "%3llu", k); /* { dg-bogus "" "unsigned long long" { xfail *-*-* } } */
|
||||
snprintf (p, 4, "%3llu", k); /* { dg-bogus "" "unsigned long long" { xfail lp64 } } */
|
||||
}
|
||||
|
|
|
@ -359,23 +359,27 @@ test_x (unsigned char uc, unsigned short us, unsigned ui)
|
|||
}
|
||||
|
||||
static void __attribute__ ((noinline, noclone))
|
||||
test_a_double (void)
|
||||
test_a_double (double d)
|
||||
{
|
||||
EQL ( 6, 7, "%a", 0.0); /* 0x0p+0 */
|
||||
EQL ( 6, 7, "%a", 1.0); /* 0x8p-3 */
|
||||
EQL ( 6, 7, "%a", 2.0); /* 0x8p-2 */
|
||||
|
||||
EQL ( 6, 7, "%.0a", 0.0); /* 0x0p+0 */
|
||||
EQL ( 6, 7, "%.0a", 1.0); /* 0x8p-3 */
|
||||
EQL ( 6, 7, "%.0a", 2.0); /* 0x8p-2 */
|
||||
EQL ( 8, 9, "%.1a", 3.0); /* 0xc.0p-2 */
|
||||
EQL ( 9, 10, "%.2a", 4.0); /* 0xa.00p-1 */
|
||||
EQL ( 9, 10, "%.2a", 4.0); /* 0x8.00p-1 */
|
||||
EQL (10, 11, "%.3a", 5.0); /* 0xa.000p-1 */
|
||||
|
||||
/* d is in [ 0, -DBL_MAX ] */
|
||||
RNG ( 6, 10, 11, "%.0a", d); /* 0x0p+0 ... -0x2p+1023 */
|
||||
RNG ( 6, 12, 13, "%.1a", d); /* 0x0p+0 ... -0x2.0p+1023 */
|
||||
RNG ( 6, 13, 14, "%.2a", d); /* 0x0p+0 ... -0x2.00p+1023 */
|
||||
}
|
||||
|
||||
static void __attribute__ ((noinline, noclone))
|
||||
test_a_long_double (void)
|
||||
{
|
||||
EQL ( 6, 7, "%La", 0.0L); /* 0x0p+0 */
|
||||
EQL ( 6, 7, "%La", 1.0L); /* 0x8p-3 */
|
||||
EQL ( 6, 7, "%La", 2.0L); /* 0x8p-2 */
|
||||
|
||||
EQL ( 6, 7, "%.0La", 0.0L); /* 0x0p+0 */
|
||||
EQL ( 6, 7, "%.0La", 1.0L); /* 0x8p-3 */
|
||||
EQL ( 6, 7, "%.0La", 2.0L); /* 0x8p-2 */
|
||||
EQL ( 8, 9, "%.1La", 3.0L); /* 0xc.0p-2 */
|
||||
EQL ( 9, 10, "%.2La", 4.0L); /* 0xa.00p-1 */
|
||||
}
|
||||
|
@ -525,7 +529,7 @@ int main (void)
|
|||
test_d_i (0xdeadbeef, 0xdeadbeefL);
|
||||
test_x ('?', 0xdead, 0xdeadbeef);
|
||||
|
||||
test_a_double ();
|
||||
test_a_double (0.0);
|
||||
test_e_double ();
|
||||
test_f_double ();
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue