#define HAVE_MMAP

#include <glib.h>

/* header, to land in gfileutils.h */

typedef struct _GReadFile GReadFile;

typedef enum
{
  G_READFILE_FLAG_WRITEABLE = 1 << 0,  /* read-write memory */
  G_READFILE_FLAG_BINARY = 1 << 1,     /* no need to null-terminate */
  G_READFILE_FLAG_FAST = 1 << 2,       /* do not try reading if mapping fails */
} GReadFileFlags;

GReadFile *g_readfile_open         (const gchar   *filename,
                                    GReadFileFlags flags,
                                    GError       **error);
gchar     *g_readfile_get_contents (const GReadFile *readfile);
gsize      g_readfile_get_length   (const GReadFile *readfile);
void       g_readfile_close        (GReadFile *readfile);









/* implementation, to land in gfileutils.c */

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <glib/gstdio.h>
#include <glib/gi18n.h>
#include <fcntl.h>
#include <errno.h>


#ifdef HAVE_MMAP
#include <sys/mman.h>
#endif


struct _GReadFile {
  gchar   *contents;
  gsize    length;

  guint    mapped : 1;      /* Is mapped or read? */
  guint    nullpage : 1;    /* Mapped a null page right after? */
  guint    alloced : 1;     /* Alloced the buffer? */
};

GReadFile *g_readfile_open (const gchar   *filename,
                            GReadFileFlags flags,
                            GError       **error)
{
  GReadFile *readfile;
  int fd;
  struct stat st;

  g_return_val_if_fail (filename != NULL, NULL);
  

  fd = g_open (filename, O_RDONLY);
  if (fd == -1)
    return NULL;

  readfile = g_new0(GReadFile, 1);

  if (fstat (fd, &st) == -1)
    {
      int save_errno = errno;
      gchar *display_filename = g_filename_display_name (filename);

      g_set_error (error,
                   G_FILE_ERROR,
                   g_file_error_from_errno (save_errno),
                   _("Failed to get attributes of file '%s': fstat() failed: %s"),
                   display_filename, 
		   g_strerror (save_errno));
      g_free (display_filename);
      goto out;
    }

#ifdef HAVE_MMAP
  {
    int mmap_prot;
    int mmap_flags;

    if (!S_ISREG(st.st_mode))
      goto read_it;
      
    readfile->length = st.st_size;

    mmap_prot = PROT_READ;
    if (flags & G_READFILE_FLAG_WRITEABLE)
      {
        mmap_prot |= PROT_WRITE;
      }

#ifdef MAP_COPY
    mmap_flags = MAP_COPY;
#else
    mmap_flags = MAP_PRIVATE;
#endif

    if (readfile->length)
      {
	readfile->contents = (gchar *) mmap (NULL, readfile->length,
					     mmap_prot, mmap_flags, fd, 0);
	if (readfile->contents == MAP_FAILED)
	  goto read_it;
      }
    else
      {
	/* Zero-length file.*/
        if (0 == (flags & G_READFILE_FLAG_BINARY))
	  {
            if (flags & G_READFILE_FLAG_WRITEABLE)
	      {
	        readfile->contents = g_new0 (gchar, 1);
	        readfile->alloced = 1;
	      }
	    else
	      {
	        readfile->contents = "";
	      }
	  }
        /* Done */
	goto ret;
      }

    {
      /* Handle null-termination if requested.  Here is what we do:
       * If a binary mapping is requested, or the file length is not a
       * multiple of pagesize, we are fine, a null byte is guaranteed.
       * Otherwise, we try to map a single byte (a page in reality) of
       * MAP_ANONYMOUS just after the mapped file.  If that fails, we
       * clean up and go to read it.
       */

      int pagesize = sysconf (_SC_PAGESIZE);

      if (0 == (flags & G_READFILE_FLAG_BINARY) &&
	  0 == (readfile->length % pagesize))
	{
#ifdef MAP_ANONYMOUS
	  /* Bad luck :(.  Got to /try to/ allocate a null page. */
	  gchar *page = (gchar *) mmap (readfile->contents + readfile->length,
					1, mmap_prot,
					MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS, 
					fd, 0);

	  /* But wait.  It can become worse. */
	  if (page == MAP_FAILED ||
	      page != readfile->contents + readfile->length)
	    {
	      /* Yes, we are paranoid:  We don't trust MAP_FIXED */
	      if (page != MAP_FAILED)
		munmap (page, 1);
#endif

	      /* Clean up mapping and go read it */
	      munmap (readfile->contents, readfile->length);
	      goto read_it;

#ifdef MAP_ANONYMOUS
	    }
	  readfile->nullpage = 1;
#endif
	}
    }

    readfile->mapped = 1;
    /* Done */
    goto ret;
  }
#endif


  goto read_it; /* shut gcc up */

  /* Mapping has failed, so try to read the file. */
 read_it:

  if (flags & G_READFILE_FLAG_FAST)
    {
      g_set_error (error,
		   G_FILE_ERROR,
		   G_FILE_ERROR_FAILED,
		   _("Mapping failed, and a fast read-file is requested."));
      goto out;
    }

  if (!
#ifdef G_OS_WIN32
/*       get_contents_win32  # replace when landed */
       g_file_get_contents
#else
/*       get_contents_posix  # replace when landed */
       g_file_get_contents
#endif
       (filename, &(readfile->contents), &(readfile->length), error))
    goto out;

  readfile->alloced = 1;
  /* Done */
  goto ret; /* shut gcc up */

 ret:
  close (fd);
  return readfile;

 out:
  g_free (readfile);
  close (fd);
  return NULL;
}

gchar *
g_readfile_get_contents (const GReadFile *readfile)
{
  return readfile->contents;
}

gsize
g_readfile_get_length (const GReadFile *readfile)
{
  return readfile->length;
}

void
g_readfile_close (GReadFile *readfile)
{
#ifdef HAVE_MMAP
  if (readfile->mapped)
    {
      if (readfile->nullpage)
        {
          munmap (readfile->contents + readfile->length, 1);
        }

      munmap (readfile->contents, readfile->length);
    }
  else
#endif
  if (readfile->alloced)
    {
      g_free (readfile->contents);
    }

  g_free(readfile);
}














/* test driver */

#include <string.h>
#include <stdio.h>

gboolean
test_readfile (const gchar *filename, gboolean writeable, gboolean binary,
               const gchar *buf, gsize len)
{
  GReadFile *h;

  g_fprintf (stderr, "testing %s %s readfile len %d ",
            writeable ? "writeable" : "readonly",
	    binary ? "binary" : "text",
	    len);

  h = g_readfile_open ("temp",
                       (writeable?G_READFILE_FLAG_WRITEABLE:0)|
		       (binary?G_READFILE_FLAG_BINARY:0),
		       NULL);
  if (!h)
    {
      g_fprintf (stderr, "failed, null returned\n");
      return FALSE;
    }


  if (len != g_readfile_get_length (h))
    {
      g_fprintf (stderr, "failed, length does not match: got %d\n",
	  g_readfile_get_length (h));
      return FALSE;
    }

  if (0 != memcmp (buf, g_readfile_get_contents (h), len + binary?0:1))
    {
      g_fprintf (stderr, "failed, buffers don't match\n");
      return FALSE;
    }

  if (writeable)
    memcpy (g_readfile_get_contents (h), buf, len + binary?0:1);

  g_fprintf (stderr, "passed%s%s%s\n",
            h->mapped?", mapped":"",
            h->alloced?", alloced":"",
            h->nullpage?", null page mapped":"");

  g_readfile_close (h);

  return TRUE;
}

void
test (const gsize len)
{
  gchar *buf = g_malloc (len + 1);
  gsize i;
  int fd;
  gchar filename[] = "temp";

  for (i = 0; i < len; i++)
    buf[i] = '0' + (i % 10);
  buf[len] = '\0';

  unlink (filename);
  fd = g_open (filename, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);
  if (fd == -1)
    {
      perror (filename);
      return;
    }
  write (fd, buf, len);
  close (fd);

  test_readfile (filename, TRUE , TRUE , buf, len);
  test_readfile (filename, FALSE, TRUE , buf, len);
  test_readfile (filename, TRUE , FALSE, buf, len);
  test_readfile (filename, FALSE, FALSE, buf, len);

  g_free (buf);
}


int
main()
{
  int pagesize = sysconf (_SC_PAGESIZE);

  test(0);
  test(pagesize - 100);
  test(pagesize - 1);
  test(pagesize);
  test(pagesize + 1);
  test(pagesize + 100);
  test(10 * pagesize - 100);
  test(10 * pagesize - 1);
  test(10 * pagesize);
  test(10 * pagesize + 1);
  test(10 * pagesize + 100);
  test(10 * pagesize + 1000);

  return 0;
}

