/*******************************************************************************
*
* MODULE: memalloc
*
********************************************************************************
*
* DESCRIPTION: Memory allocation and tracing routines
*
********************************************************************************
*
* Copyright (c) 2002-2024 Marcus Holland-Moritz. All rights reserved.
* This program is free software; you can redistribute it and/or modify
* it under the same terms as Perl itself.
*
*******************************************************************************/

#include "ccattr.h"

#if defined(DEBUG_MEMALLOC)

#include "memalloc.h"

#ifdef DEBUG_MEMALLOC

# ifdef AUTOPURGE_MEMALLOC
#  include <string.h>
# endif

# ifdef UTIL_FORMAT_CHECK

#  define DEBUG( flag, out ) debug_check out

static void debug_check( const char *str, ... )
            __attribute__(( __format__( __printf__, 1, 2 ), __noreturn__ ));

# else

#  define DEBUG( flag, out )                                         \
          do {                                                       \
            if( gs_dbfunc && ((DB_MEMALLOC_ ## flag) & gs_dbflags) ) \
              gs_dbfunc out ;                                        \
          } while(0)

# endif

static void (*gs_dbfunc)(const char *, ...) = NULL;
static unsigned long gs_dbflags             = 0;

#else /* !DEBUG_MEMALLOC */

# define DEBUG( flag, out )

#endif

#ifndef MEMALLOC_MAX_DIAG_DIST
# define MEMALLOC_MAX_DIAG_DIST     256
#endif

#ifndef MEMALLOC_BUCKET_SIZE_INCR
# define MEMALLOC_BUCKET_SIZE_INCR    4
#endif

#ifndef MEMALLOC_HASH_OFFSET
# define MEMALLOC_HASH_OFFSET         4
#endif

#ifndef MEMALLOC_HASH_BITS
# define MEMALLOC_HASH_BITS           8
#endif

#ifndef HEX_BYTES_PER_LINE
# define HEX_BYTES_PER_LINE          16
#endif

#if defined(DEBUG_MEMALLOC) && defined(TRACE_MEMALLOC)

#ifndef MEM_TRACE_REALLOC
# define MEM_TRACE_REALLOC   realloc
#endif

#ifndef MEM_TRACE_FREE
# define MEM_TRACE_FREE      free
#endif

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <assert.h>
#include <limits.h>

#ifndef ULONG_MAX
# define ULONG_MAX ((1<<(8*sizeof(unsigned long)))-1)
#endif

#define HASH_BUCKET( ptr )  ((((unsigned long)(ptr)) >> MEMALLOC_HASH_OFFSET)  \
                             & ((1 << MEMALLOC_HASH_BITS) - 1))

#define TRACE_MSG( msg )    (void) (gs_dbfunc ? gs_dbfunc : trace_msg) msg

#define free_slot( p )                                                         \
        do {                                                                   \
          gs_memstat.free++;                                                   \
          gs_memstat.total_blocks--;                                           \
          gs_memstat.total_bytes -= p->size;                                   \
          if( MEMALLOC_FLAG(env.check_freed) )                                 \
            (p)->freed = 1;                                                    \
          else {                                                               \
            (p)->ptr   = NULL;                                                 \
            (p)->size  = 0;                                                    \
          }                                                                    \
        } while(0)

typedef struct {
  const void    *ptr;
  const char    *file;
  int            line;
  unsigned       freed:1;
  size_t         size;
  unsigned long  serial;
} MemTrace;

typedef struct {
  int        size;
  MemTrace  *block;
} MemTraceBucket;

static struct {
  unsigned long alloc;
  unsigned long free;
  unsigned long total_blocks;
  unsigned long total_bytes;
  unsigned long max_total_blocks;
  unsigned long max_total_bytes;
  size_t        min_alloc;
  size_t        max_alloc;
  double        avg_alloc;
} gs_memstat;

static struct {
  int initialized;
  struct {
    int soft_assert;
    int check_freed;
    int show_dumps;
  } env;
} gs_flags;

static int gs_stat_level = -1;
static unsigned long  gs_serial = 0;
static MemTraceBucket gs_trace[1<<MEMALLOC_HASH_BITS];

static void trace_msg( const char *fmt, ... )
{
  va_list l;
  va_start(l, fmt);
  vfprintf(stderr, fmt, l);
  va_end(l);
}

#define MEMALLOC_FLAG( x ) \
        (gs_flags.initialized ? gs_flags.x : (update_flags(), gs_flags.x))

static void update_flags( void )
{
  char *str;
  gs_flags.env.soft_assert = (str=getenv("MEMALLOC_SOFT_ASSERT")) && atoi(str);
  gs_flags.env.check_freed = (str=getenv("MEMALLOC_CHECK_FREED")) && atoi(str);
  gs_flags.env.show_dumps  = (str=getenv("MEMALLOC_SHOW_DUMPS"))  && atoi(str);
}

static void trace_leaks( void )
{
  int b, i, level = gs_stat_level;
  long min_buck, max_buck, empty_buckets = 0;
  unsigned long bytes_used = 0;
  MemTraceBucket *buck;

  assert( gs_memstat.alloc - gs_memstat.free == gs_memstat.total_blocks );

  if( level < 0 && (gs_memstat.total_blocks != 0 || gs_memstat.total_bytes != 0) )
    level = 1;

  if( level >= 1 ) {
    if( gs_serial == ULONG_MAX )
      TRACE_MSG(("*** serial number overflow, results may be inaccurate ***\n"));

    TRACE_MSG(("--------------------------------\n"));

    if( level >= 2 )
      TRACE_MSG((" serials used   : %lu\n", gs_serial));

    TRACE_MSG((" total allocs   : %lu\n", gs_memstat.alloc));
    TRACE_MSG((" total frees    : %lu\n", gs_memstat.free));
    TRACE_MSG((" max mem blocks : %lu\n", gs_memstat.max_total_blocks));
    TRACE_MSG((" max mem usage  : %lu byte%s\n", gs_memstat.max_total_bytes,
                 gs_memstat.max_total_bytes == 1 ? "" : "s"));

    if( gs_memstat.max_total_blocks > 0 ) {
      TRACE_MSG((" smallest block : %d byte%s\n", gs_memstat.min_alloc,
                   gs_memstat.min_alloc == 1 ? "" : "s"));
      TRACE_MSG((" largest block  : %d byte%s\n", gs_memstat.max_alloc,
                   gs_memstat.max_alloc == 1 ? "" : "s"));
      TRACE_MSG((" average block  : %.1f bytes\n",
                   gs_memstat.avg_alloc/(double)gs_memstat.alloc));
    }

    if( gs_memstat.total_blocks > 0 ) {
      TRACE_MSG((" memory leakage : %d byte%s in %d block%s\n",
                   gs_memstat.total_bytes, gs_memstat.total_bytes == 1 ? "" : "s",
                   gs_memstat.total_blocks, gs_memstat.total_blocks == 1 ? "" : "s"
               ));
    }

    TRACE_MSG(("--------------------------------\n"));
  }

  min_buck = max_buck = gs_trace[0].size;

  for( b = 0, buck = &gs_trace[0]; (unsigned)b < sizeof(gs_trace)/sizeof(gs_trace[0]); ++b, ++buck ) {
    if( level >= 3 ) {
      TRACE_MSG(("bucket %d used %d bytes in %d blocks\n",
                 b, buck->size*sizeof(MemTrace), buck->size));
    }

    if( buck->size < min_buck )
      min_buck = buck->size;
    if( buck->size > max_buck )
      max_buck = buck->size;

    if( buck->block != NULL ) {
      assert( buck->size > 0 );
      bytes_used += buck->size*sizeof(MemTrace);

      for( i = 0; i < buck->size; ++i ) {
        MemTrace *p = &buck->block[i];
        if( p->ptr != NULL && !p->freed ) {
          TRACE_MSG(("(%d) leaked %d bytes at %p allocated in %s:%d\n",
                     p->serial, p->size, p->ptr, p->file, p->line));

          gs_memstat.total_blocks--;
          gs_memstat.total_bytes -= p->size;

#ifdef MEMALLOC_FREE_BLOCKS_AT_EXIT
          UTIL_FREE( (void *) p->ptr );
#endif
        }
      }

#ifdef MEMALLOC_FREE_BLOCKS_AT_EXIT
      MEM_TRACE_FREE( buck->block );
#endif
    }
    else {
      assert( buck->size == 0 );
      empty_buckets++;
    }
  }

  if( level >= 2 ) {
    TRACE_MSG(("memalloc tracing used %d bytes in %d buckets (%d empty)\n",
               bytes_used, b, empty_buckets));
    TRACE_MSG(("min/max bucket size was %d/%d blocks\n", min_buck, max_buck));
  }

  assert( gs_memstat.total_blocks == 0 );
  assert( gs_memstat.total_bytes == 0 );
}

static inline MemTrace *get_empty_slot( const void *ptr )
{
  MemTraceBucket *buck;
  MemTrace *p;
  int i, pos = -1;

  assert( ptr != NULL );

  buck = &gs_trace[ HASH_BUCKET(ptr) ];

  for( i = 0; i < buck->size; ++i ) {
    p = &buck->block[i];
    if( p->ptr == ptr ) {
      if( p->freed ) {
        p->ptr   = NULL;
        p->size  = 0;
        p->freed = 0;
        return p;
      }
      return NULL;
    }
    if( pos < 0 && p->ptr == NULL )
      pos = i;
  }

  if( pos < 0 )
    pos = buck->size;

  if( pos >= buck->size ) {
    buck->size  = pos + MEMALLOC_BUCKET_SIZE_INCR;
    buck->block = MEM_TRACE_REALLOC( buck->block, buck->size * sizeof(MemTrace) );
    if( buck->block == NULL ) {
      fprintf(stderr, "panic: out of memory in get_empty_slot()\n");
      abort();
    }
    for( p = &buck->block[i = pos]; i < buck->size; ++i, ++p ) {
      p->ptr   = NULL;
      p->size  = 0;
      p->freed = 0;
    }
  }

  return &buck->block[pos];
}

static inline MemTrace *find_slot( const void *ptr )
{
  MemTraceBucket *buck;
  MemTrace *p;
  int pos;

  buck = &gs_trace[ HASH_BUCKET(ptr) ];

  for( pos = 0; pos < buck->size; ++pos ) {
    p = &buck->block[pos];
    if( p->ptr == ptr )
      return p;
  }

  return NULL;
}

static void hex_dump( const void *ptr, size_t len )
{
  const unsigned char *px = ptr;
  unsigned long pos = 0;

  for( pos = 0; pos < len; pos += HEX_BYTES_PER_LINE ) {
    int i;
    TRACE_MSG(("%08lX ", pos));
    for( i = 0; pos+i < len && i < HEX_BYTES_PER_LINE; i++ )
      TRACE_MSG(("%s%02X", i%4 ? " " : "  ", px[pos+i]));
    for( ; i < HEX_BYTES_PER_LINE; i++ )
      TRACE_MSG(("%s  ", i%4 ? " " : "  "));
    TRACE_MSG((" "));
    for( i = 0; pos+i < len && i < HEX_BYTES_PER_LINE; i++ )
      TRACE_MSG(("%s%c", i%4 ? "" : " ", px[pos+i] < 32 || px[pos+i] > 127 ? '.' : px[pos+i]));
    TRACE_MSG(("\n"));
  }
}

static void diag_ptr( const void *ptr )
{
  const char *px = ptr;
  int b, i, delta = -1;
  MemTraceBucket *buck;
  MemTrace *best = NULL;
  enum Match { None, BeforeF, AfterF, BeforeA, AfterA, InsideF, InsideA, Freed }
             match = None;

  assert( ptr != NULL );

  for( b = 0, buck = &gs_trace[0]; (unsigned)b < sizeof(gs_trace)/sizeof(gs_trace[0]); ++b, ++buck )
    for( i = 0; i < buck->size; ++i ) {
      MemTrace *p = &buck->block[i];

      if( p->ptr != NULL ) {
        const char *ps = p->ptr;
        const char *pe = ps + p->size;
        enum Match m = None;
        int d = 0;

        if( ps == px && p->freed ) {
          m = Freed;
        }
        else if( ps <= px && px < pe ) {
          m = p->freed ? InsideF : InsideA;
        }
        else if( px >= pe ) {
          m = p->freed ? AfterF : AfterA;
          d = (px - pe) + 1;
        }
        else {
          assert( px < ps );
          m = p->freed ? BeforeF : BeforeA;
          d = ps - px;
        }

        assert( m != None );

        if( (m >  match && d < MEMALLOC_MAX_DIAG_DIST) ||
            (m == match && d < delta) ) {
          match = m;
          delta = d;
          best  = p;
        }
      }
    }

  if( match != None ) {
    const char *type, *s1, *s2;

    assert( delta >= 0 && delta < MEMALLOC_MAX_DIAG_DIST );
    assert( best != NULL );

    type = best->freed ? "a freed" : "an allocated";
    s1 = delta == 1 ? "" : "s";
    s2 = best->size == 1 ? "" : "s";

    switch( match ) {
      case BeforeF:
      case BeforeA:
        TRACE_MSG(("  %p is %d byte%s before %s block of %d byte%s at %p (%s:%d)\n",
                   ptr, delta, s1, type, best->size, s2, best->ptr, best->file, best->line));
        break;

      case AfterF:
      case AfterA:
        TRACE_MSG(("  %p is %d byte%s behind %s block of %d byte%s at %p (%s:%d)\n",
                   ptr, delta, s1, type, best->size, s2, best->ptr, best->file, best->line));
        break;

      case InsideF:
      case InsideA:
        assert( delta == 0 );
        TRACE_MSG(("  %p is inside %s block of %d byte%s at %p (%s:%d)\n",
                   ptr, type, best->size, s2, best->ptr, best->file, best->line));
        break;

      case Freed:
        assert( delta == 0 );
        TRACE_MSG(("  %p points to a block of %d byte%s already freed (%s:%d)\n",
                   ptr, best->size, s2, best->file, best->line));
        break;

      default:
        fprintf(stderr, "panic: unknown match type (%d)\n", (int) match);
        abort();
        break;
    }

    if( !best->freed && MEMALLOC_FLAG(env.show_dumps) )
      hex_dump( best->ptr, best->size );
  }
}

static void diag_range( const void *ptr, size_t size )
{
  const char *pS, *pE;
  int b, i, delta = -1, overlap = -1;
  MemTraceBucket *buck;
  MemTrace *best = NULL;
  enum Match { None, BeforeF, AfterF, BeforeA, AfterA, OverlapF, OverlapA, InsideF, Freed }
             match = None;

  assert( ptr != NULL );
  assert( size > 0 );

  pS = ptr;
  pE = pS + size;

  for( b = 0, buck = &gs_trace[0]; (unsigned)b < sizeof(gs_trace)/sizeof(gs_trace[0]); ++b, ++buck )
    for( i = 0; i < buck->size; ++i ) {
      MemTrace *p = &buck->block[i];

      if( p->ptr != NULL ) {
        const char *ps = p->ptr;
        const char *pe = ps + p->size;
        enum Match m = None;
        int d = 0, o = 0;

        /*               pS                   pE
         *                |===================|
         *                :                   :
         *          ps |--:-------------------:-----| pe           -> inside
         *  ps |---------|:pe                 :                    -> after
         *          ps |--:------| pe         :                    -> overlap
         *                : ps |---------| pe :                    -> overlap
         *                :           ps |----:----| pe            -> overlap
         *                :                   : ps |---------| pe  -> before
         *                :                   |
         *                |===================|
         */
        if( ps == pS && pe == pE && p->freed ) {
          m = Freed;
        }
        else if( ps <= pS && pe >= pE && p->freed ) {
          m = InsideF;
        }
        else if( pS <= ps && ps <= pE ) {
          m = p->freed ? OverlapF : OverlapA;
          o = pE - ps;
        }
        else if( pS <= pe && pe <= pE ) {
          m = p->freed ? OverlapF : OverlapA;
          o = pe - pS;
        }
        else if( pS > pe ) {
          m = p->freed ? AfterF : AfterA;
          d = pS - pe;
        }
        else {
          assert( pE < ps );
          m = p->freed ? BeforeF : BeforeA;
          d = ps - pE;
        }

        assert( m != None );

        if( (m >  match && d < MEMALLOC_MAX_DIAG_DIST) ||
            (m == match && (d < delta || o > overlap)) ) {
          match   = m;
          delta   = d;
          overlap = o;
          best    = p;
        }
      }
    }

  if( match != None ) {
    const char *type, *s1, *s2, *s3;

    assert( delta >= 0 && delta < MEMALLOC_MAX_DIAG_DIST );
    assert( best != NULL );

    type = best->freed ? "a freed" : "an allocated";
    s1 = delta == 1 ? "" : "s";
    s2 = best->size == 1 ? "" : "s";
    s3 = overlap == 1 ? "" : "s";

    switch( match ) {
      case BeforeF:
      case BeforeA:
        assert( overlap == 0 );
        TRACE_MSG(("  %p(%d) is %d byte%s before %s block of %d byte%s at %p (%s:%d)\n",
                   ptr, size, delta, s1, type, best->size, s2, best->ptr, best->file, best->line));
        break;

      case AfterF:
      case AfterA:
        assert( overlap == 0 );
        TRACE_MSG(("  %p(%d) is %d byte%s behind %s block of %d byte%s at %p (%s:%d)\n",
                   ptr, size, delta, s1, type, best->size, s2, best->ptr, best->file, best->line));
        break;

      case OverlapF:
      case OverlapA:
        assert( delta == 0 );
        assert( overlap > 0 );
        TRACE_MSG(("  %p(%d) overlaps %d byte%s with %s block of %d byte%s at %p (%s:%d)\n",
                   ptr, size, overlap, s3, type, best->size, s2, best->ptr, best->file, best->line));
        break;

      case InsideF:
        assert( delta == 0 );
        TRACE_MSG(("  %p(%d) is inside %s block of %d byte%s at %p (%s:%d)\n",
                   ptr, size, type, best->size, s2, best->ptr, best->file, best->line));
        break;

      case Freed:
        assert( delta == 0 );
        TRACE_MSG(("  %p(%d) matches a block already freed (%s:%d)\n",
                   ptr, size, best->file, best->line));
        break;

      default:
        fprintf(stderr, "panic: unknown match type (%d)\n", (int) match);
        abort();
        break;
    }

    if( !best->freed && MEMALLOC_FLAG(env.show_dumps) )
      hex_dump( best->ptr, best->size );
  }
}

static inline void init_trace(size_t first_alloc_size)
{
  const char *str;

  assert(gs_serial == 0);

  if( (str = getenv("MEMALLOC_STAT_LEVEL")) != NULL )
    gs_stat_level = atoi(str);

  gs_memstat.min_alloc = gs_memstat.max_alloc = first_alloc_size;
  atexit( trace_leaks );
}

static inline int trace_add( const void *ptr, size_t size, const char *file, int line )
{
  MemTrace *p;

  assert( file != NULL );

  if( ptr == NULL ) {
    if( size == 0 )
      return 1;

    TRACE_MSG(("request for %d bytes failed in %s:%d\n", size, file, line));
    return 0;
  }

  if( (p = get_empty_slot(ptr)) == NULL ) {
    TRACE_MSG(("pointer %p has already been allocated in %s:%d\n", ptr, file, line));
    return 0;
  }

  if( gs_serial == 0 )
    init_trace(size);

  gs_memstat.alloc++;
  gs_memstat.total_blocks++;
  gs_memstat.total_bytes += size;

  if( gs_memstat.total_blocks > gs_memstat.max_total_blocks )
    gs_memstat.max_total_blocks = gs_memstat.total_blocks;

  if( gs_memstat.total_bytes > gs_memstat.max_total_bytes )
    gs_memstat.max_total_bytes = gs_memstat.total_bytes;

  if( size < gs_memstat.min_alloc )
    gs_memstat.min_alloc = size;

  if( size > gs_memstat.max_alloc )
    gs_memstat.max_alloc = size;

  gs_memstat.avg_alloc += (double) size;

  p->ptr    = ptr;
  p->file   = file;
  p->line   = line;
  p->size   = size;
  p->serial = gs_serial;

  if( gs_serial < ULONG_MAX )
    gs_serial++;

  return 1;
}

static inline int trace_del( const void *ptr, const char *file, int line )
{
  MemTrace *p;

  assert( file != NULL );

  if( ptr == NULL ) {
    TRACE_MSG(("trying to free NULL pointer in %s:%d\n", ptr, file, line));
    return 0;
  }

  if( (p = find_slot(ptr)) == NULL ) {
    TRACE_MSG(("pointer %p has not yet been allocated in %s:%d\n", ptr, file, line));
    diag_ptr(ptr);
    return 0;
  }

  if( p->freed ) {
    TRACE_MSG(("pointer %p has already been freed in %s:%d\n", ptr, file, line));
    return 0;
  }

  free_slot( p );

  return 1;
}

static inline int trace_upd( const void *old, const void *ptr, size_t size, const char *file, int line )
{
  MemTrace *p;

  assert( file != NULL );

  if( old != ptr && old != NULL ) {
    if( (p = find_slot(old)) == NULL ) {
      TRACE_MSG(("pointer %p has not yet been allocated in %s:%d\n", old, file, line));
      diag_ptr(old);
    }
    else if( p->freed )
      TRACE_MSG(("pointer %p has already been freed in %s:%d\n", ptr, file, line));
    else
      free_slot( p );
  }

  if( ptr == NULL ) {
    if( size == 0 )
      return 1;

    TRACE_MSG(("request for %d bytes failed in %s:%d\n", size, file, line));
    return 0;
  }

  p = NULL;

  if( old == ptr ) {
    if( (p = find_slot(ptr)) == NULL ) {
      TRACE_MSG(("pointer %p has not yet been allocated in %s:%d\n", ptr, file, line));
      diag_ptr(ptr);
    }
    else if( p->freed ) {
      TRACE_MSG(("pointer %p has already been freed in %s:%d\n", ptr, file, line));
      p->size  = 0;
      p->freed = 0;
    }
    else {
      gs_memstat.alloc++;
      gs_memstat.free++;
    }
  }

  if( p == NULL ) {
    if( (p = get_empty_slot(ptr)) == NULL ) {
      TRACE_MSG(("pointer %p has already been allocated in %s:%d\n", ptr, file, line));
      return 0;
    }
    gs_memstat.alloc++;
    gs_memstat.total_blocks++;
    if( gs_memstat.total_blocks > gs_memstat.max_total_blocks )
      gs_memstat.max_total_blocks = gs_memstat.total_blocks;
  }

  if( gs_serial == 0 )
    init_trace(size);

  gs_memstat.total_bytes += size - p->size;

  if( gs_memstat.total_bytes > gs_memstat.max_total_bytes )
    gs_memstat.max_total_bytes = gs_memstat.total_bytes;

  if( size < gs_memstat.min_alloc )
    gs_memstat.min_alloc = size;

  if( size > gs_memstat.max_alloc )
    gs_memstat.max_alloc = size;

  gs_memstat.avg_alloc += (double) size;

  p->ptr    = ptr;
  p->file   = file;
  p->line   = line;
  p->size   = size;
  p->serial = gs_serial;

  if( gs_serial < ULONG_MAX )
    gs_serial++;

  return 1;
}

static inline int trace_check_ptr( const void *ptr, const char *file, int line )
{
  MemTrace *p;

  if( ptr != NULL && (p =find_slot(ptr)) != NULL && !p->freed )
    return 1;

  TRACE_MSG(("Assertion failed: %p is not a valid pointer in %s:%d\n", ptr, file, line));

  if( ptr != NULL )
    diag_ptr(ptr);

  if( MEMALLOC_FLAG(env.soft_assert) == 0 )
    abort();

  return 0;
}

static inline int trace_check_range( const void *ptr, size_t size, const char *file, int line )
{
  int b, i;
  MemTraceBucket *buck;

  if( ptr != NULL && size > 0 ) {
    for( b = 0, buck = &gs_trace[0]; (unsigned)b < sizeof(gs_trace)/sizeof(gs_trace[0]); ++b, ++buck )
      for( i = 0; i < buck->size; ++i ) {
        MemTrace *pmt = &buck->block[i];

        if( pmt->ptr != NULL && !pmt->freed ) {
          const char *bs = pmt->ptr;
          const char *be = bs + pmt->size;
          const char *cs = ptr;
          const char *ce = cs + size;

          int s_in_b = bs <= cs && cs <= be;
          int e_in_b = bs <= ce && ce <= be;

          if( s_in_b && e_in_b )
            return 1;
        }
      }
  }

  TRACE_MSG(("Assertion failed: %p(%d) is not a valid block in %s:%d\n", ptr, size, file, line));

  if( ptr != NULL && size > 0 )
    diag_range(ptr, size);

  if( MEMALLOC_FLAG(env.soft_assert) == 0 )
    abort();

  return 0;
}

#else

#define trace_add( ptr, size, file, line )         1
#define trace_upd( old, ptr, size, file, line )    1
#define trace_del( ptr, file, line )               1
#define trace_check_ptr( ptr, file, line )         1
#define trace_check_range( ptr, size, file, line ) 1

#endif /* defined(DEBUG_MEMALLOC) && defined(TRACE_MEMALLOC) */


#ifdef DEBUG_MEMALLOC
void *_memAlloc( size_t size, const char *file, int line )
#else
void *_memAlloc( size_t size )
#endif
{
  void *p;

#if defined(DEBUG_MEMALLOC) && defined(AUTOPURGE_MEMALLOC)
  p = UTIL_MALLOC( size + sizeof( size_t ) );
#else
  p = UTIL_MALLOC( size );
#endif

  abortMEMALLOC( "_memAlloc", size, p );

#if defined(DEBUG_MEMALLOC) && defined(AUTOPURGE_MEMALLOC)
  if( p != NULL ) {
    *((size_t *)p) = size;
    p = (void *)(((size_t *)p)+1);
  }
#endif

  (void) trace_add( p, size, file, line );
  DEBUG( TRACE, ("%s(%d):A=%d@%08lX\n", file, line, size, (unsigned long)p) );

  return p;
}

#ifdef DEBUG_MEMALLOC
void *_memCAlloc( size_t nobj, size_t size, const char *file, int line )
#else
void *_memCAlloc( size_t nobj, size_t size )
#endif
{
  void *p;

#if defined(DEBUG_MEMALLOC) && defined(AUTOPURGE_MEMALLOC)
  p = UTIL_MALLOC( nobj*size + sizeof( size_t ) );
#else
  p = UTIL_CALLOC( nobj, size );
#endif

  abortMEMALLOC( "_memCAlloc", nobj*size, p );

#if defined(DEBUG_MEMALLOC) && defined(AUTOPURGE_MEMALLOC)
  if( p != NULL ) {
    *((size_t *)p) = size;
    p = (void *)(((size_t *)p)+1);
    memset( p, 0, nobj*size );
  }
#endif

  (void) trace_add( p, nobj*size, file, line );
  DEBUG( TRACE, ("%s(%d):A=%d@%08lX\n", file, line, nobj*size, (unsigned long)p) );

  return p;
}

#ifdef DEBUG_MEMALLOC
void *_memReAlloc( void *p, size_t size, const char *file, int line )
#else
void *_memReAlloc( void *p, size_t size )
#endif
{
#if defined(DEBUG_MEMALLOC) && defined(TRACE_MEMALLOC)
  void *oldp = p;
#endif

#ifdef DEBUG_MEMALLOC
  if( p != NULL )
    DEBUG( TRACE, ("%s(%d):F=%08lX\n", file, line, (unsigned long)p) );
#endif

#if defined(DEBUG_MEMALLOC) && defined(AUTOPURGE_MEMALLOC)
  if( p != NULL ) {
    size_t old_size;

    p = (void *)(((size_t *)p)-1);
    old_size = *((size_t *)p);

    if( old_size > size )
      memset( ((char *)p) + sizeof(size_t) + size, 0xA5, old_size - size );
  }

  if( size != 0 )
    p = UTIL_REALLOC( p, size + sizeof( size_t ) );
#else
  p = UTIL_REALLOC( p, size );
#endif

  abortMEMALLOC( "_memReAlloc", size, p );

#if defined(DEBUG_MEMALLOC) && defined(AUTOPURGE_MEMALLOC)
  if( p != NULL ) {
    *((size_t *)p) = size;
    p = (void *)(((size_t *)p)+1);
  }
#endif

#ifdef DEBUG_MEMALLOC
  if( size != 0 )
    DEBUG( TRACE, ("%s(%d):A=%d@%08lX\n", file, line, size, (unsigned long)p) );

  (void) trace_upd( oldp, p, size, file, line );
#endif

  return p;
}

#ifdef DEBUG_MEMALLOC

void _memFree( void *p, const char *file, int line )
{
  DEBUG( TRACE, ("%s(%d):F=%08lX\n", file, line, (unsigned long)p) );

  if( trace_del( p, file, line ) && p ) {
#ifdef AUTOPURGE_MEMALLOC
    size_t size;
    p = (void *)(((size_t *)p)-1);
    size = *((size_t *)p);
    memset( p, 0xA5, size + sizeof( size_t ) );
#endif
    UTIL_FREE( p );
  }
}

void _assertValidPtr( const void *p, const char *file, int line )
{
  DEBUG( ASSERT, ("%s(%d):V=%08lX\n", file, line, (unsigned long)p) );
  (void) trace_check_ptr( p, file, line );
}

void _assertValidBlock( const void *p, size_t size, const char *file, int line )
{
  DEBUG( ASSERT, ("%s(%d):B=%d@%08lX\n", file, line, size, (unsigned long)p) );
  (void) trace_check_range( p, size, file, line );
}

#ifdef UTIL_FORMAT_CHECK
static void debug_check( const char *str __attribute__(( __unused__ )), ... )
{
  fprintf( stderr, "compiled with UTIL_FORMAT_CHECK, please don't run\n" );
  abort();
}
#endif

int SetDebugMemAlloc( void (*dbfunc)(const char *, ...), unsigned long dbflags )
{
  gs_dbfunc  = dbfunc;
  gs_dbflags = dbflags;
  return 1;
}

#endif /* DEBUG_MEMALLOC */

#else

/* avoid empty source file warning */
extern int _memalloc___notused __attribute__((unused));

#endif /* defined(DEBUG_MEMALLOC) */

/* ============================================================= */
/* ==================== TEST CODE FOLLOWING ==================== */
/* ============================================================= */

#ifdef MEMALLOC_TEST

#include <stdio.h>
#include <stdarg.h>
#include "memalloc.h"

static FILE *ftest;
static FILE *fdebug;

static struct {
  int debug;
  int assert;
  int check_freed;
  int stat_level;
} flags;

static void t_trace( const char *fmt, ... )
{
  va_list l;
  va_start(l, fmt);
#if defined(DEBUG_MEMALLOC) && defined(TRACE_MEMALLOC)
  vfprintf(ftest, fmt, l);
#endif
  va_end(l);
}

#if defined(DEBUG_MEMALLOC)
static void t_debug( const char *fmt, ... )
{
  va_list l;
  va_start(l, fmt);
  if( flags.debug )
    vfprintf(ftest, fmt, l);
  va_end(l);
}
#endif

static void t_assert( const char *fmt, ... )
{
  va_list l;
  va_start(l, fmt);
#if defined(DEBUG_MEMALLOC)
  if( flags.assert )
    vfprintf(ftest, fmt, l);
#endif
  va_end(l);
}

#define trc_not_alloc     t_trace("pointer %p has not yet been allocated in %s:%d\n", p, __FILE__, __LINE__)
#define trc_assP_fail     t_trace("Assertion failed: %p is not a valid pointer in %s:%d\n", p, __FILE__, __LINE__)
#define trc_assB_fail(s)  t_trace("Assertion failed: %p(%d) is not a valid block in %s:%d\n", p, s, __FILE__, __LINE__)
#define trc               t_trace
#define trc_f             if( flags.check_freed ) t_trace
#define assP              t_assert("%s(%d):V=%08lX\n", __FILE__, __LINE__, (unsigned long)p)
#define assB(s)           t_assert("%s(%d):B=%d@%08lX\n", __FILE__, __LINE__, s, (unsigned long)p)
#if defined(DEBUG_MEMALLOC)
#  define dbg(what)       t_debug("%s(%d):" #what "=%08lX\n", __FILE__, __LINE__, (unsigned long)p)
#  define dbgA(p,s)       t_debug("%s(%d):A=%d@%08lX\n", __FILE__, __LINE__, s, (unsigned long)p)
#else
#  define dbg(what)       (void)1
#  define dbgA(p,s)       (void)1
#endif

static void runtests( void )
{
  unsigned char *p, *p1;                                   int lp1;
  int i;

#define S_P1 10

  AllocF( char *, p1, S_P1 );             dbgA(p1,S_P1);   lp1 = __LINE__;

  for( i = 0; i < S_P1; i++ )
    p1[i] = (unsigned char) i;

#ifdef TRACE_MEMALLOC
  p = p1 + 1;
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc("  %p is inside an allocated block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);
  p = p1 + (S_P1-1);
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc("  %p is inside an allocated block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);
#if MEMALLOC_MAX_DIAG_DIST > 1
  p = p1 - 1;
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc("  %p is 1 byte before an allocated block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);
  p = p1 + S_P1;
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc("  %p is 1 byte behind an allocated block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);
  p = p1 - (MEMALLOC_MAX_DIAG_DIST-1);
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc("  %p is %d byte%s before an allocated block of %d bytes at %p (%s:%d)\n", p, MEMALLOC_MAX_DIAG_DIST-1,
                                                                                                MEMALLOC_MAX_DIAG_DIST-1 == 1 ? "" : "s", S_P1, p1, __FILE__, lp1);
  p = p1 + (MEMALLOC_MAX_DIAG_DIST+S_P1-2);
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc("  %p is %d byte%s behind an allocated block of %d bytes at %p (%s:%d)\n", p, MEMALLOC_MAX_DIAG_DIST-1,
                                                                                                MEMALLOC_MAX_DIAG_DIST-1 == 1 ? "" : "s", S_P1, p1, __FILE__, lp1);
#endif
  p = p1 - (MEMALLOC_MAX_DIAG_DIST);
  Free( p );                                     dbg(F);   trc_not_alloc;

  p = p1 + (MEMALLOC_MAX_DIAG_DIST+S_P1-1);
  Free( p );                                     dbg(F);   trc_not_alloc;
#endif

  p = p1;
  AssertValidPtr(p);                               assP;
  p = p1+1;
  AssertValidPtr(p);                               assP;   trc_assP_fail;
                                                           trc("  %p is inside an allocated block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);

  p = p1;
  AssertValidBlock(p,5);                        assB(5);
  p = p1-1;
  AssertValidBlock(p,5);                        assB(5);   trc_assB_fail(5);
                                                           trc("  %p(5) overlaps 4 bytes with an allocated block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);

  p = p1;
  Free( p );                                     dbg(F);

  AssertValidPtr(p);                               assP;   trc_assP_fail;
                                                           trc_f("  %p points to a block of %d bytes already freed (%s:%d)\n", p, S_P1, __FILE__, lp1);

#ifdef TRACE_MEMALLOC
  p = p1 + 1;
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc_f("  %p is inside a freed block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);
  p = p1 + (S_P1-1);
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc_f("  %p is inside a freed block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);
#if MEMALLOC_MAX_DIAG_DIST > 1
  p = p1 - 1;
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc_f("  %p is 1 byte before a freed block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);
  p = p1 + S_P1;
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc_f("  %p is 1 byte behind a freed block of %d bytes at %p (%s:%d)\n", p, S_P1, p1, __FILE__, lp1);
  p = p1 - (MEMALLOC_MAX_DIAG_DIST-1);
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc_f("  %p is %d byte%s before a freed block of %d bytes at %p (%s:%d)\n", p, MEMALLOC_MAX_DIAG_DIST-1,
                                                                                             MEMALLOC_MAX_DIAG_DIST-1 == 1 ? "" : "s", S_P1, p1, __FILE__, lp1);
  p = p1 + (MEMALLOC_MAX_DIAG_DIST+S_P1-2);
  Free( p );                                     dbg(F);   trc_not_alloc;
                                                           trc_f("  %p is %d byte%s behind a freed block of %d bytes at %p (%s:%d)\n", p, MEMALLOC_MAX_DIAG_DIST-1,
                                                                                             MEMALLOC_MAX_DIAG_DIST-1 == 1 ? "" : "s", S_P1, p1, __FILE__, lp1);
#endif
  p = p1 - (MEMALLOC_MAX_DIAG_DIST);
  Free( p );                                     dbg(F);   trc_not_alloc;

  p = p1 + (MEMALLOC_MAX_DIAG_DIST+S_P1-1);
  Free( p );                                     dbg(F);   trc_not_alloc;

  p = p1;
  Free( p );                                     dbg(F);   trc("pointer %p has %s in %s:%d\n", p, flags.check_freed ? "already been freed" : "not yet been allocated", __FILE__, __LINE__);
#endif

}

#ifdef DEBUG_MEMALLOC
static void test_dbfunc( const char *fmt, ... )
{
  va_list l;
  va_start(l, fmt);
  vfprintf(fdebug ? fdebug : stderr, fmt, l);
  va_end(l);
}
#endif

int main( void )
{
  const char *str;
  const char *file;

  if( (file = getenv("MEMALLOC_TEST_FILE")) == NULL )
    file = "test.ref";

  flags.debug       = (str=getenv("MEMALLOC_TEST_DEBUG")) && atoi(str);
  flags.assert      = (str=getenv("MEMALLOC_TEST_ASSERT")) && atoi(str);
  flags.check_freed = (str=getenv("MEMALLOC_CHECK_FREED")) && atoi(str);
  flags.stat_level  = (str=getenv("MEMALLOC_STAT_LEVEL")) ? atoi(str) : -1;

  if( (str=getenv("MEMALLOC_TEST_DEBUG_FILE")) != NULL )
    if( (fdebug = fopen(str, "w")) == NULL )
      return -1;

#ifdef DEBUG_MEMALLOC
  SetDebugMemAlloc( test_dbfunc, (flags.debug  ? DB_MEMALLOC_TRACE  : 0)
                               | (flags.assert ? DB_MEMALLOC_ASSERT : 0) );
#endif

  if( (ftest = fopen(file, "w")) == NULL )
    return -1;

  runtests();

  fclose(ftest);

#ifdef DEBUG_MEMALLOC
  SetDebugMemAlloc( NULL, 0 );
#endif

  if( fdebug )
    fclose(fdebug);

  return 0;
}
#endif