/* leakfinder - check for memory leaks
**
** Copyright (C) 2022 by Jef Poskanzer <jef@mail.acme.com>.
** All rights reserved.
**
** You may use this software however you like as long as you keep my
** name on it and don't sue me.
**
** For commentary on this license please see http://acme.com/license.html
*/

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <sysexits.h>

#include "leakfinder.h"

#ifndef NO_LEAKFINDER

typedef struct {
    int filenum;
    int line;
    int count;
    } allocrec;

static int initialized = false;
static char** filenames;
static int filenames_size, filenames_num;
static allocrec* allocrecs;
static int allocrecs_size, allocrecs_num;


static void check_init( void );
static void init( void );
static void term( void );
static int find_filename( char* filename );
static int find_allocrec( int filenum, int line );
static void oom( void );


static void check_init( void )
    {
    if ( ! initialized )
	init();
    }


static void init( void )
    {
    filenames_size = 10;
    filenames = malloc( filenames_size * sizeof(*filenames) );
    filenames_num = 0;
    allocrecs_size = 10;
    allocrecs = malloc( allocrecs_size * sizeof(*allocrecs) );
    allocrecs_num = 0;
    if ( filenames == (char**) 0 || allocrecs == (allocrec*) 0 )
	oom();
    if ( atexit( term ) != 0 )
	{
	(void) fprintf( stderr, "leakfinder: could not set exit handler\n" );
	exit( EX_SOFTWARE );
	}
    initialized = true;
    }


static void term( void )
    {
    int i, places, leaks, double_frees, count;

    if ( ! initialized )
	return;

    /* First count the places. */
    places = leaks = double_frees = 0;
    for ( i = 0; i < allocrecs_num; ++i )
	{
	count = allocrecs[i].count;
	if ( count != 0 )
	    {
	    ++places;
	    leaks += count;
	    if ( count < 0 )
		++double_frees;
	    /* (Double-frees are unlikely to be detected because they
	    ** usually cause an immediate segfault. But we might as
	    ** well check for them anyway.)
	    */
	    }
	}
    if ( places > 0 )
	{
	/* Generate a report. */
	(void) fprintf( stderr, "leakfinder: %d leaks from %d places\n", leaks, places );
	if ( double_frees > 0 )
	    (void) fprintf( stderr, "(Negative leak counts may mean you are double-freeing some things.)\n" );
	for ( i = 0; i < allocrecs_num; ++i )
	    if ( allocrecs[i].count != 0 )
		(void) fprintf( stderr, "%s line %d: %d leaks\n", filenames[allocrecs[i].filenum], allocrecs[i].line, allocrecs[i].count );
	}

    /* Free our own memory. */
    for ( i = 0; i < filenames_num; ++i )
	free( (void*) filenames[i] );
    free( (void*) filenames );
    free( (void*) allocrecs );
    }


void* leakfinder_malloc( size_t size, char* filename, int line )
    {
    int filenum, allocnum;
    size_t real_size;
    int* real_ptr;
    int* fake_ptr;

    check_init();
    filenum = find_filename( filename );
    allocnum = find_allocrec( filenum, line );
    real_size = size + sizeof(int);
    real_ptr = (int*) malloc( real_size );
    if ( real_ptr == (int*) 0 )
	return real_ptr;
    *real_ptr = allocnum;
    ++allocrecs[allocnum].count;
    fake_ptr = real_ptr + 1;
    return (void*) fake_ptr;
    }


void* leakfinder_calloc( size_t number, size_t size, char* filename, int line )
    {
    size_t fullsize;
    void* ptr;

    fullsize = number * size;
    ptr = leakfinder_malloc( fullsize, filename, line );
    if ( ptr != (void*) 0 )
	(void) memset( ptr, 0, fullsize );
    return ptr;
    }


void* leakfinder_realloc( void* ptr, size_t size, char* filename, int line )
    {
    int filenum, newallocnum, oldallocnum;
    int* fake_ptr;
    int* real_ptr;
    size_t real_size;

    check_init();
    filenum = find_filename( filename );
    newallocnum = find_allocrec( filenum, line );
    fake_ptr = (int*) ptr;
    real_ptr = fake_ptr - 1;
    oldallocnum = *real_ptr;
    --allocrecs[oldallocnum].count;

    real_size = size + sizeof(int);
    real_ptr = (int*) realloc( real_ptr, real_size );
    if ( real_ptr == (int*) 0 )
	return real_ptr;
    *real_ptr = newallocnum;
    ++allocrecs[newallocnum].count;
    fake_ptr = real_ptr + 1;
    return (void*) fake_ptr;
    }


char* leakfinder_strdup( char* str, char* filename, int line )
    {
    char* ptr;

    check_init();
    ptr = (char*) leakfinder_malloc( strlen( str ) + 1, filename, line );
    if ( ptr != (char*) 0 )
	(void) strcpy( ptr, str );
    return ptr;
    }


void leakfinder_free( void* ptr )
    {
    int* fake_ptr;
    int* real_ptr;
    int allocnum;

    check_init();
    fake_ptr = (int*) ptr;
    real_ptr = fake_ptr - 1;
    allocnum = *real_ptr;
    --allocrecs[allocnum].count;
    free( (void*) real_ptr );
    }


static int find_filename( char* filename )
    {
    int i;

    /* Is it in the table? */
    for ( i = 0; i < filenames_num; ++i )
	if ( strcmp( filename, filenames[i] ) == 0 )
	    return i;
    /* Not in the table; add it. */
    if ( filenames_num == filenames_size )
	{
	filenames_size *= 2;
	filenames = (char**) realloc( (void*) filenames, filenames_size * sizeof(*filenames) );
	if ( filenames == (char**) 0 )
	    oom();
	}
    filenames[filenames_num] = strdup( filename );
    if ( filenames[filenames_num] == (char*) 0 )
	oom();
    return filenames_num++;
    }


static int find_allocrec( int filenum, int line )
    {
    int i;

    /* Is it in the table? */
    for ( i = 0; i < allocrecs_num; ++i )
	if ( filenum == allocrecs[i].filenum && line == allocrecs[i].line )
	    return i;
    /* Not in the table; add it. */
    if ( allocrecs_num == allocrecs_size )
	{
	allocrecs_size *= 2;
	allocrecs = (allocrec*) realloc( (void*) allocrecs, allocrecs_size * sizeof(*allocrecs) );
	if ( allocrecs == (allocrec*) 0 )
	    oom();
	}
    allocrecs[allocrecs_num].filenum = filenum;
    allocrecs[allocrecs_num].line = line;
    allocrecs[allocrecs_num].count = 0;
    return allocrecs_num++;
    }


static void oom( void )
    {
    (void) fprintf( stderr, "leakfinder: out of memory\n" );
    exit( EX_SOFTWARE );
    }


#else /* NO_LEAKFINDER */

int leakfinder_we_need_at_least_one_declaration_to_avoid_a_warning;

#endif /* NO_LEAKFINDER */
