Commit 343cbf25 authored by Behdad Esfahbod's avatar Behdad Esfahbod

Bug 608196 - Overflow-safe g_new family

New public API:

g_malloc_n
g_malloc0_n
g_realloc_n
g_try_malloc_n
g_try_malloc0_n
g_try_realloc_n
parent 373f3d8b
...@@ -853,6 +853,12 @@ g_realloc ...@@ -853,6 +853,12 @@ g_realloc
g_try_malloc g_try_malloc
g_try_malloc0 g_try_malloc0
g_try_realloc g_try_realloc
g_malloc_n
g_malloc0_n
g_realloc_n
g_try_malloc_n
g_try_malloc0_n
g_try_realloc_n
<SUBSECTION> <SUBSECTION>
g_free g_free
......
...@@ -39,6 +39,7 @@ g_mem_set_vtable(). ...@@ -39,6 +39,7 @@ g_mem_set_vtable().
Allocates @n_structs elements of type @struct_type. Allocates @n_structs elements of type @struct_type.
The returned pointer is cast to a pointer to the given type. The returned pointer is cast to a pointer to the given type.
If @n_structs is 0 it returns %NULL. If @n_structs is 0 it returns %NULL.
Care is taken to avoid overflow when calculating the size of the allocated block.
</para> </para>
<para> <para>
Since the returned pointer is already casted to the right type, Since the returned pointer is already casted to the right type,
...@@ -56,6 +57,7 @@ so might hide memory allocation errors. ...@@ -56,6 +57,7 @@ so might hide memory allocation errors.
Allocates @n_structs elements of type @struct_type, initialized to 0's. Allocates @n_structs elements of type @struct_type, initialized to 0's.
The returned pointer is cast to a pointer to the given type. The returned pointer is cast to a pointer to the given type.
If @n_structs is 0 it returns %NULL. If @n_structs is 0 it returns %NULL.
Care is taken to avoid overflow when calculating the size of the allocated block.
</para> </para>
<para> <para>
Since the returned pointer is already casted to the right type, Since the returned pointer is already casted to the right type,
...@@ -73,6 +75,7 @@ so might hide memory allocation errors. ...@@ -73,6 +75,7 @@ so might hide memory allocation errors.
Reallocates the memory pointed to by @mem, so that it now has space for Reallocates the memory pointed to by @mem, so that it now has space for
@n_structs elements of type @struct_type. It returns the new address of @n_structs elements of type @struct_type. It returns the new address of
the memory, which may have been moved. the memory, which may have been moved.
Care is taken to avoid overflow when calculating the size of the allocated block.
</para> </para>
@struct_type: the type of the elements to allocate @struct_type: the type of the elements to allocate
...@@ -86,7 +89,7 @@ the memory, which may have been moved. ...@@ -86,7 +89,7 @@ the memory, which may have been moved.
Attempts to allocate @n_structs elements of type @struct_type, and returns Attempts to allocate @n_structs elements of type @struct_type, and returns
%NULL on failure. Contrast with g_new(), which aborts the program on failure. %NULL on failure. Contrast with g_new(), which aborts the program on failure.
The returned pointer is cast to a pointer to the given type. The returned pointer is cast to a pointer to the given type.
If @n_structs is 0 it returns %NULL. The function returns %NULL when @n_structs is 0 of if an overflow occurs.
</para> </para>
@struct_type: the type of the elements to allocate @struct_type: the type of the elements to allocate
...@@ -101,7 +104,7 @@ Attempts to allocate @n_structs elements of type @struct_type, initialized ...@@ -101,7 +104,7 @@ Attempts to allocate @n_structs elements of type @struct_type, initialized
to 0's, and returns %NULL on failure. Contrast with g_new0(), which aborts to 0's, and returns %NULL on failure. Contrast with g_new0(), which aborts
the program on failure. the program on failure.
The returned pointer is cast to a pointer to the given type. The returned pointer is cast to a pointer to the given type.
The function returns %NULL when @n_structs is 0. The function returns %NULL when @n_structs is 0 of if an overflow occurs.
</para> </para>
@struct_type: the type of the elements to allocate @struct_type: the type of the elements to allocate
...@@ -116,6 +119,7 @@ Attempts to reallocate the memory pointed to by @mem, so that it now has ...@@ -116,6 +119,7 @@ Attempts to reallocate the memory pointed to by @mem, so that it now has
space for @n_structs elements of type @struct_type, and returns %NULL on space for @n_structs elements of type @struct_type, and returns %NULL on
failure. Contrast with g_renew(), which aborts the program on failure. failure. Contrast with g_renew(), which aborts the program on failure.
It returns the new address of the memory, which may have been moved. It returns the new address of the memory, which may have been moved.
The function returns %NULL if an overflow occurs.
</para> </para>
@struct_type: the type of the elements to allocate @struct_type: the type of the elements to allocate
...@@ -192,6 +196,80 @@ on failure. If @mem is %NULL, behaves the same as g_try_malloc(). ...@@ -192,6 +196,80 @@ on failure. If @mem is %NULL, behaves the same as g_try_malloc().
@Returns: the allocated memory, or %NULL. @Returns: the allocated memory, or %NULL.
<!-- ##### FUNCTION g_malloc_n ##### -->
<para>
This function is similar to g_malloc(), allocating (@n_blocks * @n_block_bytes) bytes,
but care is taken to detect possible overflow during multiplication.
</para>
@n_blocks: the number of blocks to allocate
@n_block_bytes: the size of each block in bytes
@Returns: a pointer to the allocated memory
@Since: 2.24
<!-- ##### FUNCTION g_malloc0_n ##### -->
<para>
This function is similar to g_malloc0(), allocating (@n_blocks * @n_block_bytes) bytes,
but care is taken to detect possible overflow during multiplication.
</para>
@n_blocks: the number of blocks to allocate
@n_block_bytes: the size of each block in bytes
@Returns: a pointer to the allocated memory
@Since: 2.24
<!-- ##### FUNCTION g_realloc_n ##### -->
<para>
This function is similar to g_realloc(), allocating (@n_blocks * @n_block_bytes) bytes,
but care is taken to detect possible overflow during multiplication.
</para>
@mem: the memory to reallocate
@n_blocks: the number of blocks to allocate
@n_block_bytes: the size of each block in bytes
@Returns: the new address of the allocated memory
@Since: 2.24
<!-- ##### FUNCTION g_try_malloc_n ##### -->
<para>
This function is similar to g_try_malloc(), allocating (@n_blocks * @n_block_bytes) bytes,
but care is taken to detect possible overflow during multiplication.
</para>
@n_blocks: the number of blocks to allocate
@n_block_bytes: the size of each block in bytes
@Returns: the allocated memory, or %NULL.
@Since: 2.24
<!-- ##### FUNCTION g_try_malloc0_n ##### -->
<para>
This function is similar to g_try_malloc0(), allocating (@n_blocks * @n_block_bytes) bytes,
but care is taken to detect possible overflow during multiplication.
</para>
@n_blocks: the number of blocks to allocate
@n_block_bytes: the size of each block in bytes
@Returns: the allocated memory, or %NULL
@Since: 2.24
<!-- ##### FUNCTION g_try_realloc_n ##### -->
<para>
This function is similar to g_try_realloc(), allocating (@n_blocks * @n_block_bytes) bytes,
but care is taken to detect possible overflow during multiplication.
</para>
@mem: previously-allocated memory, or %NULL.
@n_blocks: the number of blocks to allocate
@n_block_bytes: the size of each block in bytes
@Returns: the allocated memory, or %NULL.
@Since: 2.24
<!-- ##### FUNCTION g_free ##### --> <!-- ##### FUNCTION g_free ##### -->
<para> <para>
Frees the memory pointed to by @mem. Frees the memory pointed to by @mem.
......
...@@ -720,13 +720,19 @@ g_markup_collect_attributes ...@@ -720,13 +720,19 @@ g_markup_collect_attributes
g_free g_free
g_malloc G_GNUC_MALLOC g_malloc G_GNUC_MALLOC
g_malloc0 G_GNUC_MALLOC g_malloc0 G_GNUC_MALLOC
g_malloc_n G_GNUC_MALLOC
g_malloc0_n G_GNUC_MALLOC
g_mem_is_system_malloc g_mem_is_system_malloc
g_mem_profile g_mem_profile
g_mem_set_vtable g_mem_set_vtable
g_realloc g_realloc
g_realloc_n
g_try_malloc G_GNUC_MALLOC g_try_malloc G_GNUC_MALLOC
g_try_malloc0 G_GNUC_MALLOC g_try_malloc0 G_GNUC_MALLOC
g_try_malloc_n G_GNUC_MALLOC
g_try_malloc0_n G_GNUC_MALLOC
g_try_realloc g_try_realloc
g_try_realloc_n
#ifndef G_DISABLE_DEPRECATED #ifndef G_DISABLE_DEPRECATED
g_allocator_free g_allocator_free
g_allocator_new g_allocator_new
......
...@@ -203,11 +203,11 @@ g_try_malloc (gsize n_bytes) ...@@ -203,11 +203,11 @@ g_try_malloc (gsize n_bytes)
gpointer gpointer
g_try_malloc0 (gsize n_bytes) g_try_malloc0 (gsize n_bytes)
{ {
gpointer mem; gpointer mem;
mem = g_try_malloc (n_bytes); mem = g_try_malloc (n_bytes);
if (mem) if (mem)
memset (mem, 0, n_bytes); memset (mem, 0, n_bytes);
...@@ -229,6 +229,97 @@ g_try_realloc (gpointer mem, ...@@ -229,6 +229,97 @@ g_try_realloc (gpointer mem,
return NULL; return NULL;
} }
#define SIZE_OVERFLOWS(a,b) (G_UNLIKELY ((a) > G_MAXSIZE / (b)))
#undef g_malloc_n
gpointer
g_malloc_n (gsize n_blocks,
gsize n_block_bytes)
{
if (SIZE_OVERFLOWS (n_blocks, n_block_bytes))
{
if (G_UNLIKELY (!g_mem_initialized))
g_mem_init_nomessage();
g_error ("%s: overflow allocating %"G_GSIZE_FORMAT"*%"G_GSIZE_FORMAT" bytes",
G_STRLOC, n_blocks, n_block_bytes);
}
return g_malloc (n_blocks * n_block_bytes);
}
#undef g_malloc0_n
gpointer
g_malloc0_n (gsize n_blocks,
gsize n_block_bytes)
{
if (SIZE_OVERFLOWS (n_blocks, n_block_bytes))
{
if (G_UNLIKELY (!g_mem_initialized))
g_mem_init_nomessage();
g_error ("%s: overflow allocating %"G_GSIZE_FORMAT"*%"G_GSIZE_FORMAT" bytes",
G_STRLOC, n_blocks, n_block_bytes);
}
return g_malloc0 (n_blocks * n_block_bytes);
}
#undef g_realloc_n
gpointer
g_realloc_n (gpointer mem,
gsize n_blocks,
gsize n_block_bytes)
{
if (SIZE_OVERFLOWS (n_blocks, n_block_bytes))
{
if (G_UNLIKELY (!g_mem_initialized))
g_mem_init_nomessage();
g_error ("%s: overflow allocating %"G_GSIZE_FORMAT"*%"G_GSIZE_FORMAT" bytes",
G_STRLOC, n_blocks, n_block_bytes);
}
return g_realloc (mem, n_blocks * n_block_bytes);
}
#undef g_try_malloc_n
gpointer
g_try_malloc_n (gsize n_blocks,
gsize n_block_bytes)
{
if (SIZE_OVERFLOWS (n_blocks, n_block_bytes))
return NULL;
return g_try_malloc (n_blocks * n_block_bytes);
}
#undef g_try_malloc0_n
gpointer
g_try_malloc0_n (gsize n_blocks,
gsize n_block_bytes)
{
if (SIZE_OVERFLOWS (n_blocks, n_block_bytes))
return NULL;
return g_try_malloc0 (n_blocks * n_block_bytes);
}
#undef g_try_realloc_n
gpointer
g_try_realloc_n (gpointer mem,
gsize n_blocks,
gsize n_block_bytes)
{
if (SIZE_OVERFLOWS (n_blocks, n_block_bytes))
return NULL;
return g_try_realloc (mem, n_blocks * n_block_bytes);
}
static gpointer static gpointer
fallback_calloc (gsize n_blocks, fallback_calloc (gsize n_blocks,
gsize n_block_bytes) gsize n_block_bytes)
......
...@@ -48,40 +48,102 @@ typedef struct _GMemVTable GMemVTable; ...@@ -48,40 +48,102 @@ typedef struct _GMemVTable GMemVTable;
/* Memory allocation functions /* Memory allocation functions
*/ */
void g_free (gpointer mem);
gpointer g_malloc (gsize n_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE(1); gpointer g_malloc (gsize n_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE(1);
gpointer g_malloc0 (gsize n_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE(1); gpointer g_malloc0 (gsize n_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE(1);
gpointer g_realloc (gpointer mem, gpointer g_realloc (gpointer mem,
gsize n_bytes) G_GNUC_WARN_UNUSED_RESULT; gsize n_bytes) G_GNUC_WARN_UNUSED_RESULT;
void g_free (gpointer mem);
gpointer g_try_malloc (gsize n_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE(1); gpointer g_try_malloc (gsize n_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE(1);
gpointer g_try_malloc0 (gsize n_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE(1); gpointer g_try_malloc0 (gsize n_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE(1);
gpointer g_try_realloc (gpointer mem, gpointer g_try_realloc (gpointer mem,
gsize n_bytes) G_GNUC_WARN_UNUSED_RESULT; gsize n_bytes) G_GNUC_WARN_UNUSED_RESULT;
gpointer g_malloc_n (gsize n_blocks,
gsize n_block_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE2(1,2);
gpointer g_malloc0_n (gsize n_blocks,
gsize n_block_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE2(1,2);
gpointer g_realloc_n (gpointer mem,
gsize n_blocks,
gsize n_block_bytes) G_GNUC_WARN_UNUSED_RESULT;
gpointer g_try_malloc_n (gsize n_blocks,
gsize n_block_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE2(1,2);
gpointer g_try_malloc0_n (gsize n_blocks,
gsize n_block_bytes) G_GNUC_MALLOC G_GNUC_ALLOC_SIZE2(1,2);
gpointer g_try_realloc_n (gpointer mem,
gsize n_blocks,
gsize n_block_bytes) G_GNUC_WARN_UNUSED_RESULT;
/* avoid the overflow check if we can determine at compile-time that no
* overflow happens. */
#if defined (__GNUC__) && (__GNUC__ >= 2) && defined (__OPTIMIZE__)
# define _G_MALLOC_N(n_blocks, n_block_bytes, func_1, func_n) \
(__extension__ ({ \
gsize __a = (gsize) (n_blocks); \
gsize __b = (gsize) (n_block_bytes); \
gpointer __p; \
if (__builtin_constant_p (__a) && __a == 1) \
__p = func_1 (__b); \
else if (__builtin_constant_p (__b) && __b == 1) \
__p = func_1 (__a); \
else if (__builtin_constant_p (__a) && \
__builtin_constant_p (__b) && \
__a <= G_MAXSIZE / __b) \
__p = func_1 (__a * __b); \
else \
__p = func_n (__a, __b); \
__p; \
}))
# define _G_REALLOC_N(mem, n_blocks, n_block_bytes, func_1, func_n) \
(__extension__ ({ \
gsize __a = (gsize) (n_blocks); \
gsize __b = (gsize) (n_block_bytes); \
gpointer __p = (gpointer) (mem); \
if (__builtin_constant_p (__a) && __a == 1) \
__p = func_1 (__p, __b); \
else if (__builtin_constant_p (__b) && __b == 1) \
__p = func_1 (__p, __a); \
else if (__builtin_constant_p (__a) && \
__builtin_constant_p (__b) && \
__a <= G_MAXSIZE / __b) \
__p = func_1 (__p, __a * __b); \
else \
__p = func_n (__p, __a, __b); \
__p; \
}))
# define g_malloc_n(n_blocks,n_block_bytes) _G_MALLOC_N (n_blocks, n_block_bytes, g_malloc, g_malloc_n)
# define g_malloc0_n(n_blocks,n_block_bytes) _G_MALLOC_N (n_blocks, n_block_bytes, g_malloc0, g_malloc0_n)
# define g_realloc_n(mem,n_blocks,n_block_bytes) _G_REALLOC_N (mem, n_blocks, n_block_bytes, g_realloc, g_realloc_n)
# define g_try_malloc_n(n_blocks,n_block_bytes) _G_MALLOC_N (n_blocks, n_block_bytes, g_try_malloc, g_try_malloc_n)
# define g_try_malloc0_n(n_blocks,n_block_bytes) _G_MALLOC_N (n_blocks, n_block_bytes, g_try_malloc0, g_try_malloc0_n)
# define g_try_realloc_n(mem,n_blocks,n_block_bytes) _G_REALLOC_N (mem, n_blocks, n_block_bytes, g_try_realloc, g_try_realloc_n)
#endif
/* Convenience memory allocators /* Convenience memory allocators
*/ */
#define g_new(struct_type, n_structs) \
((struct_type *) g_malloc (((gsize) sizeof (struct_type)) * ((gsize) (n_structs))))
#define g_new0(struct_type, n_structs) \
((struct_type *) g_malloc0 (((gsize) sizeof (struct_type)) * ((gsize) (n_structs))))
#define g_renew(struct_type, mem, n_structs) \
((struct_type *) g_realloc ((mem), ((gsize) sizeof (struct_type)) * ((gsize) (n_structs))))
#define g_try_new(struct_type, n_structs) \ #define _G_NEW(struct_type, n_structs, _g_malloc_n) \
((struct_type *) g_try_malloc (((gsize) sizeof (struct_type)) * ((gsize) (n_structs)))) ((struct_type *) _g_malloc_n ((n_structs), sizeof (struct_type)))
#define g_try_new0(struct_type, n_structs) \ #define _G_RENEW(struct_type, mem, n_structs, _g_realloc_n) \
((struct_type *) g_try_malloc0 (((gsize) sizeof (struct_type)) * ((gsize) (n_structs)))) ((struct_type *) _g_realloc_n ((mem), (n_structs), sizeof (struct_type)))
#define g_try_renew(struct_type, mem, n_structs) \
((struct_type *) g_try_realloc ((mem), ((gsize) sizeof (struct_type)) * ((gsize) (n_structs)))) #define g_new(struct_type, n_structs) _G_NEW (struct_type, n_structs, g_malloc_n)
#define g_new0(struct_type, n_structs) _G_NEW (struct_type, n_structs, g_malloc0_n)
#define g_renew(struct_type, mem, n_structs) _G_RENEW (struct_type, mem, n_structs, g_realloc_n)
#define g_try_new(struct_type, n_structs) _G_NEW (struct_type, n_structs, g_try_malloc_n)
#define g_try_new0(struct_type, n_structs) _G_NEW (struct_type, n_structs, g_try_malloc0_n)
#define g_try_renew(struct_type, mem, n_structs) _G_RENEW (struct_type, mem, n_structs, g_try_realloc_n)
/* Memory allocation virtualization for debugging purposes /* Memory allocation virtualization for debugging purposes
* g_mem_set_vtable() has to be the very first GLib function called * g_mem_set_vtable() has to be the very first GLib function called
* if being used * if being used
*/ */
struct _GMemVTable struct _GMemVTable {
{
gpointer (*malloc) (gsize n_bytes); gpointer (*malloc) (gsize n_bytes);
gpointer (*realloc) (gpointer mem, gpointer (*realloc) (gpointer mem,
gsize n_bytes); gsize n_bytes);
......
...@@ -50,6 +50,9 @@ hostutils_LDADD = $(progs_ldadd) ...@@ -50,6 +50,9 @@ hostutils_LDADD = $(progs_ldadd)
TEST_PROGS += gvariant TEST_PROGS += gvariant
gvariant_LDADD = $(progs_ldadd) gvariant_LDADD = $(progs_ldadd)
TEST_PROGS += mem-overflow
mem_overflow_LDADD = $(progs_ldadd)
if OS_UNIX if OS_UNIX
# some testing of gtester funcitonality # some testing of gtester funcitonality
......
/* Unit tests for g
* Copyright (C) 2010 Red Hat, Inc.
*
* This work is provided "as is"; redistribution and modification
* in whole or in part, in any medium, physical or electronic is
* permitted without restriction.
*
* This work is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* In no event shall the authors or contributors be liable for any
* direct, indirect, incidental, special, exemplary, or consequential
* damages (including, but not limited to, procurement of substitute
* goods or services; loss of use, data, or profits; or business
* interruption) however caused and on any theory of liability, whether
* in contract, strict liability, or tort (including negligence or
* otherwise) arising in any way out of the use of this software, even
* if advised of the possibility of such damage.
*/
#include "glib.h"
#include <stdlib.h>
static void
mem_overflow (void)
{
gsize a = G_MAXSIZE / 10 + 10;
gsize b = 10;
gpointer p, q;
typedef char X[10];
#define CHECK_PASS(P) p = (P); g_assert (p == NULL);
#define CHECK_FAIL(P) p = (P); g_assert (p != NULL);
CHECK_PASS (g_try_malloc_n (a, a));
CHECK_PASS (g_try_malloc_n (a, b));
CHECK_PASS (g_try_malloc_n (b, a));
CHECK_FAIL (g_try_malloc_n (b, b));
CHECK_PASS (g_try_malloc0_n (a, a));
CHECK_PASS (g_try_malloc0_n (a, b));
CHECK_PASS (g_try_malloc0_n (b, a));
CHECK_FAIL (g_try_malloc0_n (b, b));
q = g_malloc (1);
CHECK_PASS (g_try_realloc_n (q, a, a));
CHECK_PASS (g_try_realloc_n (q, a, b));
CHECK_PASS (g_try_realloc_n (q, b, a));
CHECK_FAIL (g_try_realloc_n (q, b, b));
free (p);
CHECK_PASS (g_try_new (X, a));
CHECK_FAIL (g_try_new (X, b));
CHECK_PASS (g_try_new0 (X, a));
CHECK_FAIL (g_try_new0 (X, b));
q = g_try_malloc (1);
CHECK_PASS (g_try_renew (X, q, a));
CHECK_FAIL (g_try_renew (X, q, b));
free (p);
#undef CHECK_EQ
#undef CHECK_NEQ
#define CHECK_FAIL(P) if (g_test_trap_fork (0, G_TEST_TRAP_SILENCE_STDERR)) { p = (P); exit (0); } g_test_trap_assert_failed();
#define CHECK_PASS(P) if (g_test_trap_fork (0, G_TEST_TRAP_SILENCE_STDERR)) { p = (P); exit (0); } g_test_trap_assert_passed();
CHECK_FAIL (g_malloc_n (a, a));
CHECK_FAIL (g_malloc_n (a, b));
CHECK_FAIL (g_malloc_n (b, a));
CHECK_PASS (g_malloc_n (b, b));
CHECK_FAIL (g_malloc0_n (a, a));
CHECK_FAIL (g_malloc0_n (a, b));
CHECK_FAIL (g_malloc0_n (b, a));
CHECK_PASS (g_malloc0_n (b, b));
q = g_malloc (1);
CHECK_FAIL (g_realloc_n (q, a, a));
CHECK_FAIL (g_realloc_n (q, a, b));
CHECK_FAIL (g_realloc_n (q, b, a));
CHECK_PASS (g_realloc_n (q, b, b));
free (q);
CHECK_FAIL (g_new (X, a));
CHECK_PASS (g_new (X, b));
CHECK_FAIL (g_new0 (X, a));
CHECK_PASS (g_new0 (X, b));
q = g_malloc (1);
CHECK_FAIL (g_renew (X, q, a));
CHECK_PASS (g_renew (X, q, b));
free (q);
}
int
main (int argc,
char *argv[])
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/mem/overflow", mem_overflow);
return g_test_run();
}
/* Unit tests for gstring /* Unit tests for gprintf
* Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
* *
* This work is provided "as is"; redistribution and modification * This work is provided "as is"; redistribution and modification
......
/* Unit tests for gstring /* Unit tests for grand
* Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
* *
* This work is provided "as is"; redistribution and modification * This work is provided "as is"; redistribution and modification
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment