/* dirname.c
 *
 * $Id: dirname.c,v 1.2 2007/03/08 23:15:58 keithmarshall Exp $
 *
 * Provides an implementation of the "dirname" function, conforming
 * to SUSv3, with extensions to accommodate Win32 drive designators,
 * and suitable for use on native Microsoft(R) Win32 platforms.
 *
 * Written by Keith Marshall <keithmarshall@users.sourceforge.net>
 *
 * This is free software.  You may redistribute and/or modify it as you
 * see fit, without restriction of copyright.
 *
 * This software is provided "as is", in the hope that it may be useful,
 * but WITHOUT WARRANTY OF ANY KIND, not even any implied warranty of
 * MERCHANTABILITY, nor of FITNESS FOR ANY PARTICULAR PURPOSE.  At no
 * time will the author accept any form of liability for any damages,
 * however caused, resulting from the use of this software.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libgen.h>
#include <locale.h>

#ifndef __cdecl  /* If compiling on any non-Win32 platform ... */
#define __cdecl  /* this may not be defined.                   */
#endif

char * __cdecl
dirname(char *path)
{
  static char *retfail = NULL;
  size_t len;
  /* to handle path names for files in multibyte character locales,
   * we need to set up LC_CTYPE to match the host file system locale.  */
  char *locale = setlocale (LC_CTYPE, NULL);

  if (locale != NULL)
    locale = strdup (locale);
  setlocale (LC_CTYPE, "");

  if (path && *path)
    {
      /* allocate sufficient local storage space,
       * in which to create a wide character reference copy of path.  */
      wchar_t refcopy[1 + (len = mbstowcs (NULL, path, 0))];
      /* create the wide character reference copy of path */
      wchar_t *refpath = refcopy;

      len = mbstowcs (refpath, path, len);
      refcopy[len] = L'\0';
      /* SUSv3 identifies a special case, where path is exactly equal to "//";
       * (we will also accept "\\" in the Win32 context, but not "/\" or "\/",
       *  and neither will we consider paths with an initial drive designator).
       * For this special case, SUSv3 allows the implementation to choose to
       * return "/" or "//", (or "\" or "\\", since this is Win32); we will
       * simply return the path unchanged, (i.e. "//" or "\\").  */
      if (len > 1 && (refpath[0] == L'/' || refpath[0] == L'\\'))
        {
	  if (refpath[1] == refpath[0] && refpath[2] == L'\0')
	    {
	      setlocale (LC_CTYPE, locale);
	      free (locale);
	      return path;
	    }
        }
      /* For all other cases ...
       * step over the drive designator, if present ...  */
      else if (len > 1 && refpath[1] == L':')
        {
	  /* FIXME: maybe should confirm *refpath is a valid drive designator.  */
	  refpath += 2;
        }
      /* check again, just to ensure we still have a non-empty path name ... */
      if (*refpath)
        {
#	undef  basename
#	define basename __the_basename		/* avoid shadowing. */
	  /* reproduce the scanning logic of the "basename" function
	   * to locate the basename component of the current path string,
	   * (but also remember where the dirname component starts).  */
	  wchar_t *refname, *basename;
	  for (refname = basename = refpath; *refpath; ++refpath)
	    {
	      if (*refpath == L'/' || *refpath == L'\\')
	        {
		  /* we found a dir separator ...
		   * step over it, and any others which immediately follow it.  */
		  while (*refpath == L'/' || *refpath == L'\\')
		    ++refpath;
		  /* if we didn't reach the end of the path string ... */
		  if (*refpath)
		    /* then we have a new candidate for the base name.  */
		    basename = refpath;
		  else
		    /* we struck an early termination of the path string,
		     * with trailing dir separators following the base name,
		     * so break out of the for loop, to avoid overrun.  */
		    break;
	        }
	    }
	  /* now check,
	   * to confirm that we have distinct dirname and basename components.  */
	  if (basename > refname)
	    {
	      /* and, when we do ...
	       * backtrack over all trailing separators on the dirname component,
	       * (but preserve exactly two initial dirname separators, if identical),
	       * and add a NUL terminator in their place.  */
	      do --basename;
	      while (basename > refname && (*basename == L'/' || *basename == L'\\'));
	      if (basename == refname && (refname[0] == L'/' || refname[0] == L'\\')
		  && refname[1] == refname[0] && refname[2] != L'/' && refname[2] != L'\\')
		++basename;
	      *++basename = L'\0';
	      /* if the resultant dirname begins with EXACTLY two dir separators,
	       * AND both are identical, then we preserve them.  */
	      refpath = refcopy;
	      while ((*refpath == L'/' || *refpath == L'\\'))
		++refpath;
	      if ((refpath - refcopy) > 2 || refcopy[1] != refcopy[0])
		refpath = refcopy;
	      /* and finally ...
	       * we remove any residual, redundantly duplicated separators from the dirname,
	       * reterminate, and return it.  */
	      refname = refpath;
	      while (*refpath)
	        {
		  if ((*refname++ = *refpath) == L'/' || *refpath++ == L'\\')
		    {
		      while (*refpath == L'/' || *refpath == L'\\')
			++refpath;
		    }
	        }
	      *refname = L'\0';
	      /* finally ...
	       * transform the resolved dirname back into the multibyte char domain,
	       * restore the caller's locale, and return the resultant dirname.  */
	      if ((len = wcstombs( path, refcopy, len )) != (size_t)(-1))
		path[len] = '\0';
	    }
	  else
	    {
	      /* either there were no dirname separators in the path name,
	       * or there was nothing else ...  */
	      if (*refname == L'/' || *refname == L'\\')
	        {
		  /* it was all separators, so return one.  */
		  ++refname;
	        }
	      else
	        {
		  /* there were no separators, so return '.'.  */
		  *refname++ = L'.';
	        }
	      /* add a NUL terminator, in either case,
	       * then transform to the multibyte char domain,
	       * using our own buffer.  */
	      *refname = L'\0';
	      retfail = realloc (retfail, len = 1 + wcstombs (NULL, refcopy, 0));
	      wcstombs (path = retfail, refcopy, len);
	    }
	  /* restore caller's locale, clean up, and return the resolved dirname.  */
	  setlocale (LC_CTYPE, locale);
	  free (locale);
	  return path;
        }
#	undef  basename
    }
  /* path is NULL, or an empty string; default return value is "." ...
   * return this in our own buffer, regenerated by wide char transform,
   * in case the caller trashed it after a previous call.
   */
  retfail = realloc (retfail, len = 1 + wcstombs (NULL, L".", 0));
  wcstombs (retfail, L".", len);
  /* restore caller's locale, clean up, and return the default dirname.  */
  setlocale (LC_CTYPE, locale);
  free (locale);
  return retfail;
}
