/*
//
// Copyright (c) 1993-2015 Robert McNeel & Associates. All rights reserved.
// OpenNURBS, Rhinoceros, and Rhino3D are registered trademarks of Robert
// McNeel & Associates.
//
// THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY.
// ALL IMPLIED WARRANTIES OF FITNESS FOR ANY PARTICULAR PURPOSE AND OF
// MERCHANTABILITY ARE HEREBY DISCLAIMED.
//				
// For complete openNURBS copyright information see <http://www.opennurbs.org>.
//
////////////////////////////////////////////////////////////////
*/

#include "opennurbs.h"

#if !defined(ON_COMPILING_OPENNURBS)
// This check is included in all opennurbs source .c and .cpp files to insure
// ON_COMPILING_OPENNURBS is defined when opennurbs source is compiled.
// When opennurbs source is being compiled, ON_COMPILING_OPENNURBS is defined 
// and the opennurbs .h files alter what is declared and how it is declared.
#error ON_COMPILING_OPENNURBS must be defined when compiling opennurbs
#endif

#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
// November 2015: Visual Studo 2013 (and probably others)
// Shlwapi.h and Shlobj.h are not included in opennurbs_system.h 
// because the have gems like "#define small ..." (Thank You Microsoft!).
// Turns out there is plenty of code that uses opennurbs where crazy 
// developers thought "small" would be a reasonable name for a local
// variable.  Reminds me of dealing with AutoDesk's old #define X 0
// in their headers from 20 years ago. 
#pragma ON_PRAGMA_WARNING_BEFORE_DIRTY_INCLUDE
#include <Shlwapi.h>
#pragma ON_PRAGMA_WARNING_AFTER_DIRTY_INCLUDE
#pragma ON_PRAGMA_WARNING_BEFORE_DIRTY_INCLUDE
#include <Shlobj.h>
#pragma ON_PRAGMA_WARNING_AFTER_DIRTY_INCLUDE
#if defined(_M_X64) && defined(WIN32) && defined(WIN64)
// Shlwapi.h, Shlobj.h and perhaps others, unconditionally define WIN32
#undef WIN32
#endif
#endif

#if defined(ON_RUNTIME_APPLE)
#include "unistd.h" //for unlink
#endif

///////////////////////////////////////////////////////////////////////////////

void ON_String::SplitPath(
  const char* path,
  ON_String* drive,
  ON_String* dir,
  ON_String* fname,
  ON_String* ext
  )
{
  ON_FileSystemPath::SplitPath(
    path,
    drive,
    dir,
    fname,
    ext
    );
}

bool ON_FileSystemPath::IsDirectorySeparator(
  char c,
  bool bAllowAlternate
  )
{
  return (c == ON_FileSystemPath::DirectorySeparatorAsChar || (bAllowAlternate && c == ON_FileSystemPath::DirectorySeparatorAsChar));
}

bool ON_FileSystemPath::IsDirectorySeparator(
  wchar_t c,
  bool bAllowAlternate
  )
{
  return (c == ON_FileSystemPath::DirectorySeparator || (bAllowAlternate && c == ON_FileSystemPath::AlternateDirectorySeparator));
}

void ON_FileSystemPath::SplitPath(
    const char* path,
    ON_String* drive,
    ON_String* dir,
    ON_String* file_name_stem,
    ON_String* ext
    )
{
  const char* dr = 0;
  const char* d = 0;
  const char* f = 0;
  const char* e = 0;

  // Use local path in case drive, dir, file_name_stem or ext are being reused.
  const ON_String local_path(path);
  path = static_cast<const char*>(local_path);
  on_splitpath(path,&dr,&d,&f,&e);

  if ( 0 != drive )
  {
    if ( 0 != dr )
    {
      int length;
      if ( 0 != d )
        length = (int)(d-dr);
      else if ( 0 != f )
        length = (int)(f-dr);
      else if ( 0 != e )
        length = (int)(e-dr);
      else
        length = ON_String::Length(dr);
      *drive = ON_String(dr,length);
    }
    else
      drive->Empty();
  }

  if ( 0 != dir )
  {
    if ( 0 != d )
    {
      int length;
      if ( 0 != f )
        length = (int)(f-d);
      else if ( 0 != e )
        length = (int)(e-d);
      else
        length = ON_String::Length(d);

      *dir = ON_String(d,length);
    }
    else
      dir->Empty();
  }

  if ( 0 != file_name_stem )
  {
    if ( 0 != f )
    {
      int length;
      if ( 0 != e )
        length = (int)(e-f);
      else
        length = ON_String::Length(f);

      *file_name_stem = ON_String(f,length);
    }
    else
      file_name_stem->Empty();
  }

  if ( 0 != ext )
  {
    *ext = e;
  }
}

void ON_wString::SplitPath(
  const char* path,
  ON_wString* drive,
  ON_wString* dir,
  ON_wString* fname,
  ON_wString* ext
  )
{
  ON_FileSystemPath::SplitPath(
    path,
    drive,
    dir,
    fname,
    ext
    );
}

void ON_FileSystemPath::SplitPath(
    const char* path,
    ON_wString* drive,
    ON_wString* dir,
    ON_wString* file_name_stem,
    ON_wString* ext
    )
{
  const char* dr = 0;
  const char* d = 0;
  const char* f = 0;
  const char* e = 0;

  // Use local path in case drive, dir, file_name_stem or ext are being reused.
  const ON_String local_path(path);
  path = static_cast<const char*>(local_path);
  on_splitpath(path,&dr,&d,&f,&e);

  if ( 0 != drive )
  {
    if ( 0 != dr )
    {
      int length;
      if ( 0 != d )
        length = (int)(d-dr);
      else if ( 0 != f )
        length = (int)(f-dr);
      else if ( 0 != e )
        length = (int)(e-dr);
      else
        length = ON_String::Length(dr);

      *drive = ON_wString(dr,length);
    }
    else
      drive->Empty();
  }

  if ( 0 != dir )
  {
    if ( 0 != d )
    {
      int length;
      if ( 0 != f )
        length = (int)(f-d);
      else if ( 0 != e )
        length = (int)(e-d);
      else
        length = ON_String::Length(d);

      *dir = ON_wString(d,length);
    }
    else
      dir->Empty();
  }

  if ( 0 != file_name_stem )
  {
    if ( 0 != f )
    {
      int length;
      if ( 0 != e )
        length = (int)(e-f);
      else
        length = ON_String::Length(f);

      *file_name_stem = ON_wString(f,length);
    }
    else
      file_name_stem->Empty();
  }

  if ( 0 != ext )
  {
    *ext = e;
  }
}


void ON_wString::SplitPath(
  const wchar_t* path,
  ON_wString* drive,
  ON_wString* dir,
  ON_wString* fname,
  ON_wString* ext
  )
{
  ON_FileSystemPath::SplitPath( 
    path,
    drive,
    dir,
    fname,
    ext
    );
}

void ON_FileSystemPath::SplitPath(
  const wchar_t* path,
  ON_wString* drive,
  ON_wString* dir,
  ON_wString* file_name_stem_and_extension
)
{
  const wchar_t* dr = 0;
  const wchar_t* d = 0;
  const wchar_t* f = 0;
  const wchar_t* e = 0;

  // Use local path in case drive, dir, file_name_stem or ext are being reused.
  const ON_wString local_path(path);
  path = static_cast<const wchar_t*>(local_path);
  on_wsplitpath(path,&dr,&d,&f,&e);

  if ( 0 != drive )
  {
    if ( 0 != dr )
    {
      int length;
      if ( 0 != d )
        length = (int)(d-dr);
      else if ( 0 != f )
        length = (int)(f-dr);
      else if ( 0 != e )
        length = (int)(e-dr);
      else
        length = ON_wString::Length(dr);

      *drive = ON_wString(dr,length);
    }
    else
      drive->Empty();
  }

  if ( 0 != dir )
  {
    if ( 0 != d )
    {
      int length;
      if ( 0 != f )
        length = (int)(f-d);
      else if ( 0 != e )
        length = (int)(e-d);
      else
        length = ON_wString::Length(d);
      *dir = ON_wString(d,length);
    }
    else
      dir->Empty();
  }

  if ( 0 != file_name_stem_and_extension )
  {
    if ( 0 != f )
    {
      *file_name_stem_and_extension = f;
    }
    else if ( 0 != e )
    {
      // "C:/dir/.abc" returns ".abc"
      *file_name_stem_and_extension = e;
    }
    else
      file_name_stem_and_extension->Empty();
  }
}


void ON_FileSystemPath::SplitPath( 
  const wchar_t* path,
  ON_wString* drive,
  ON_wString* dir,
  ON_wString* file_name_stem,
  ON_wString* ext
  )
{
  const wchar_t* dr = 0;
  const wchar_t* d = 0;
  const wchar_t* f = 0;
  const wchar_t* e = 0;

  // Use local path in case drive, dir, file_name_stem or ext are being reused.
  const ON_wString local_path(path);
  path = static_cast<const wchar_t*>(local_path);
  on_wsplitpath(path,&dr,&d,&f,&e);

  if ( 0 != drive )
  {
    if ( 0 != dr )
    {
      int length;
      if ( 0 != d )
        length = (int)(d-dr);
      else if ( 0 != f )
        length = (int)(f-dr);
      else if ( 0 != e )
        length = (int)(e-dr);
      else
        length = ON_wString::Length(dr);

      *drive = ON_wString(dr,length);
    }
    else
      drive->Empty();
  }

  if ( 0 != dir )
  {
    if ( 0 != d )
    {
      int length;
      if ( 0 != f )
        length = (int)(f-d);
      else if ( 0 != e )
        length = (int)(e-d);
      else
        length = ON_wString::Length(d);
      *dir = ON_wString(d,length);
    }
    else
      dir->Empty();
  }

  if ( 0 != file_name_stem )
  {
    if ( 0 != f )
    {
      int length;
      if ( 0 != e )
        length = (int)(e-f);
      else
        length = ON_wString::Length(f);
      *file_name_stem = ON_wString(f,length);
    }
    else
      file_name_stem->Empty();
  }

  if ( 0 != ext )
  {
    *ext = e;
  }
}

const ON_wString ON_FileSystemPath::VolumeFromPath(
  const wchar_t* path
)
{
  ON_wString volume;
  ON_FileSystemPath::SplitPath(path, &volume, nullptr, nullptr, nullptr);
  return volume;
}

const ON_wString ON_FileSystemPath::DirectoryFromPath(
  const wchar_t* path
)
{
  ON_wString directory;
  ON_FileSystemPath::SplitPath(path, nullptr, &directory, nullptr, nullptr);
  return directory;
}

const ON_wString ON_FileSystemPath::VolumeAndDirectoryFromPath(
  const wchar_t* path
)
{
  ON_wString volume;
  ON_wString directory;
  ON_FileSystemPath::SplitPath(path, &volume, &directory, nullptr, nullptr);
  const ON_wString volume_and_directory( volume + directory);
  return volume_and_directory;
}

const ON_wString ON_FileSystemPath::FileNameFromPath(
  const wchar_t* path,
  bool bIncludeExtension
)
{
  ON_wString fname;
  if ( bIncludeExtension )
    ON_FileSystemPath::SplitPath(path, nullptr, nullptr, &fname);
  else
    ON_FileSystemPath::SplitPath(path, nullptr, nullptr, &fname, nullptr);
  return fname;
}

const ON_wString ON_FileSystemPath::FileNameExtensionFromPath(
  const wchar_t* path
)
{
  ON_wString ext;
  ON_FileSystemPath::SplitPath(path, nullptr, nullptr, nullptr, &ext);
  return ext;
}

static bool IsAtoZ(int c)
{
  return ((c >= 'A' && c <= 'Z') ||  (c >= 'a' && c <= 'z'));
}

static bool Is0to9(int c)
{
  return (c >= '0' && c <= '9');
}

static bool IsPermittedInPathName(int c)
{
  if ( c >= 0 && c < ON_wString::Space )
    return false;
  switch (c)
  {
  case '/':
  case '\\':
  case ':':
  case '<':
  case '>':
  case '"':
  case '?':
  case '*':
  case '|':
  //case 127:
    return false;
  }
  return true;
}

static bool IsPermittedInHostName(int c)
{
  return IsPermittedInPathName(c);
}


static bool IsDirSep(int c)
{
  switch (c)
  {
  case '/':
  case '\\':
    return true;
  }
  return false;
}

static bool IsDotDir(const wchar_t* path)
{
  return (nullptr != path && '.' == path[0] && IsDirSep(path[1]));
}

static bool IsDotDotDir(const wchar_t* path)
{
  return (nullptr != path && '.' == path[0] && '.' == path[1] && IsDirSep(path[2]));
}


bool ON_FileSystemPath::IsRelativePath(
  const wchar_t* path
  )
{
  return ON_FileSystemPath::IsRelativePath(path, 0);
}

const ON_wString ON_FileSystemPath::CurrentDirectory(
  bool bWithTrailingDirectorySeparator
  )
{
#if defined(ON_RUNTIME_WIN)

  wchar_t* directory = nullptr;
  const unsigned int directory_capacity = 2018;
  const size_t sizeof_directory = directory_capacity * sizeof(directory[0]);
  directory = (wchar_t*)onmalloc(sizeof_directory);
  memset(directory, 0, sizeof_directory);

  DWORD rc = ::GetCurrentDirectory(directory_capacity - 1, directory);
  ON_wString fullpath = directory;
  onfree(directory);

  if (rc <= 0 || fullpath.IsEmpty() )
  {
    ON_ERROR("Windows API ::GetCurrentDirectory() failed.");
    return ON_wString::EmptyString;
  }

  if (bWithTrailingDirectorySeparator)
    fullpath += ON_FileSystemPath::DirectorySeparator;
  return fullpath;

#elif defined(ON_RUNTIME_APPLE)

  // TODO implement for Apple OS's
  ON_ERROR("ON_FileSystemPath::CurrentDirectory() not implemented.");
  return ON_wString::EmptyString;   

#else

  // unsupported OS
  ON_ERROR("ON_FileSystemPath::CurrentDirectory() not implemented.");
  return ON_wString::EmptyString;   
#endif
}

const ON_wString ON_FileSystemPath::RemoveVolumeName(
  const wchar_t* path,
  ON_wString* volume_name
)
{
  const ON_wString local_path = ON_FileSystemPath::CleanPath(path);
  path = static_cast<const wchar_t*>(local_path);
  const wchar_t* vol = nullptr;
  const wchar_t* dir = nullptr;
  const wchar_t* fname = nullptr;
  const wchar_t* fext = nullptr;
  on_wsplitpath(path, &vol, &dir, &fname, &fext);
  if (nullptr == dir)
  {
    if (nullptr != fname)
      dir = fname;
    else if (nullptr != fext)
      dir = fext;
  }

  if (nullptr != volume_name)
  {
    size_t length
      = (nullptr != vol && nullptr != dir && vol < dir)
      ? (int)(dir - vol)
      : 0;
    if (length > 0)
      *volume_name = ON_wString(vol, (int)length);
    else
      *volume_name = ON_wString::EmptyString;
  }
  return ON_wString(dir);
}

const ON_wString ON_FileSystemPath::RemoveFileName(
  const wchar_t* path,
  ON_wString* file_name
)
{
  const ON_wString local_path = ON_FileSystemPath::CleanPath(path);
  path = static_cast<const wchar_t*>(local_path);
  const wchar_t* vol = nullptr;
  const wchar_t* dir = nullptr;
  const wchar_t* fname = nullptr;
  on_wsplitpath(path, &vol, &dir, &fname, nullptr);
  
  const size_t length
    = (nullptr != fname && nullptr != path && path <= fname)
    ? (int)(fname - path)
    : local_path.Length();

  if (nullptr != file_name)
    *file_name = fname;

  return ON_wString(path,(int)length);
}


const ON_wString ON_FileSystemPath::CombinePaths(
  const wchar_t* left_side,
  bool bLeftSideContainsFileName,
  const wchar_t* right_side,
  bool bRightSideContainsFileName,
  bool bAppendTrailingDirectorySeparator
)
{
  ON_wString lhs_fname;
  ON_wString lhs
    = bLeftSideContainsFileName
    ? ON_FileSystemPath::RemoveFileName(left_side,&lhs_fname)
    : ON_FileSystemPath::CleanPath(left_side);

  ON_wString rhs_fname;
  ON_wString rhs 
    = bRightSideContainsFileName
    ? ON_FileSystemPath::RemoveFileName(right_side,&rhs_fname)
    : ON_FileSystemPath::CleanPath(right_side);

  ON_wString rhs_volume;
  if ( lhs.IsNotEmpty() )
    rhs = ON_FileSystemPath::RemoveVolumeName(rhs, &rhs_volume);

  if (rhs.IsNotEmpty() && ON_FileSystemPath::IsDirectorySeparator(rhs[0], true))
  {
    const ON_wString tmp(static_cast<const wchar_t*>(rhs) + 1);
    rhs = tmp;
  }

  ON_wString path = lhs;
  bool bPathEndsDirectorySeparator 
    = path.IsNotEmpty()
    && ON_FileSystemPath::IsDirectorySeparator(path[path.Length() - 1], true);
  
  if (rhs.IsNotEmpty())
  {
    if (path.IsNotEmpty() && false == bPathEndsDirectorySeparator)
      path += ON_FileSystemPath::DirectorySeparator;
    path += rhs;

    if (lhs.IsNotEmpty() && ON_FileSystemPath::IsRelativePath(rhs))
      path = ON_FileSystemPath::CleanPath(path);

    bPathEndsDirectorySeparator 
      = path.IsNotEmpty()
      && ON_FileSystemPath::IsDirectorySeparator(path[path.Length() - 1], true);
  }

  if (path.IsNotEmpty() && rhs_fname.IsNotEmpty())
    bAppendTrailingDirectorySeparator = true;

  if ((bPathEndsDirectorySeparator ? 0 : 1) != (bAppendTrailingDirectorySeparator ? 0 : 1))
  {
    if (bAppendTrailingDirectorySeparator)
    {
      const wchar_t* vol = nullptr;
      const wchar_t* dir = nullptr;
      // on_wsplitpath is called to avoid appending a directory separator to a 
      on_wsplitpath(static_cast<const wchar_t*>(path), &vol, &dir, nullptr, nullptr);
      if (nullptr != dir && false == ON_FileSystemPath::IsDirectorySeparator(path[path.Length() - 1], true))
        path += ON_FileSystemPath::DirectorySeparator;
    }
    else if ( bPathEndsDirectorySeparator )
    {
      path.SetLength(path.Length() - 1);
    }
  }

  path += rhs_fname;

  return path;
}

bool ON_FileSystemPath::IsRelativePath(
  const wchar_t* path,
  const wchar_t directory_separator
  )
{
  for (;;)
  {
    if (nullptr == path)
      break;
    if ('.' != *path)
      break;
    path++;
    if ('.' == *path)
      path++;
    if (0 != directory_separator)
    {
      if (directory_separator != *path)
        break;
    }
    else
    {
      if (ON_FileSystemPath::DirectorySeparator != *path && ON_FileSystemPath::AlternateDirectorySeparator != *path)
        break;
    }

    return true;
  }
  return false;
}

const ON_wString ON_FileSystemPath::CleanPath(
  const wchar_t* dirty_path
  )
{
  bool bTrimLeft = true;
  bool bTrimRight = true;
  bool bAllowWindowsUNCHostNameOrDiskLetter = true;
  bool bDeleteWindowsUNCHostNameOrDiskLetter = (ON_wString::Backslash != ON_FileSystemPath::DirectorySeparator);
  const wchar_t directory_separator = ON_FileSystemPath::DirectorySeparator;
  return ON_FileSystemPath::CleanPath(
    bTrimLeft,
    bTrimRight,
    bAllowWindowsUNCHostNameOrDiskLetter,
    bDeleteWindowsUNCHostNameOrDiskLetter,
    directory_separator,
    dirty_path
    );
}

static const ON_wString ON_wString_CleanPathFailed()
{
  return ON_wString::EmptyString;
}

const ON_wString ON_FileSystemPath::CleanPath(
  bool bTrimLeft,
  bool bTrimRight,
  bool bAllowWindowsUNCHostNameOrDiskLetter,
  bool bDeleteWindowsUNCHostNameOrDiskLetter,
  const wchar_t directory_separator,
  const wchar_t* dirty_path
)
{
  return ON_FileSystemPath::CleanPath(
    bTrimLeft,
    bTrimRight,
    bAllowWindowsUNCHostNameOrDiskLetter,
    bDeleteWindowsUNCHostNameOrDiskLetter,
    false,
    directory_separator,
    dirty_path
  );
}

const ON_wString ON_FileSystemPath::CleanPath(
  bool bTrimLeft,
  bool bTrimRight,
  bool bAllowWindowsUNCHostNameOrDiskLetter,
  bool bDeleteWindowsUNCHostNameOrDiskLetter,
  bool bExpandUser,
  const wchar_t directory_separator,
  const wchar_t* dirty_path
  )
{
  ON_wString local_dirty_path(dirty_path);
  if ( bTrimLeft )
    local_dirty_path.TrimLeft();
  if ( bTrimRight )
    local_dirty_path.TrimRight();
  if ( local_dirty_path.IsEmpty() )
    return ON_wString_CleanPathFailed();
  if (bExpandUser)
    local_dirty_path = ON_FileSystemPath::ExpandUser(local_dirty_path);

  dirty_path = local_dirty_path;

  const bool bIsUNCHostName 
    = bAllowWindowsUNCHostNameOrDiskLetter 
    && local_dirty_path.Length() >= 3
    && ON_wString::Backslash == local_dirty_path[0]
    && ON_wString::Backslash == local_dirty_path[1]
    && IsPermittedInHostName(local_dirty_path[2])
    && (IsAtoZ(local_dirty_path[2]) || Is0to9(local_dirty_path[2]) || local_dirty_path[2] > 127)
    ;

  const bool bIsWindowsDrive 
    = bAllowWindowsUNCHostNameOrDiskLetter
    && (false == bIsUNCHostName)
    && local_dirty_path.Length() >= 3
    && ':' == local_dirty_path[1]
    && IsAtoZ(local_dirty_path[0])
    ;

  ON_wString clean_path(dirty_path);
  wchar_t* clean_head = clean_path.Array();
  wchar_t* clean_start = clean_head;
  if (bIsUNCHostName)
  {
    clean_start += 3; // skip \\ and first charater of host name
    
    // skip rest of host name
    while ( IsPermittedInHostName(*clean_start) )
      clean_start++;    
    if (false == IsDirSep(*clean_start))
      return ON_wString_CleanPathFailed();
  }
  else if (bIsWindowsDrive)
  {
    // Windows drive letter = capital letter
    *clean_start = ON_wString::MapCharacterOrdinal(ON_StringMapOrdinalType::UpperOrdinal,*clean_start);
    clean_start += 2; // Skip drive letter and colon
    if (false == IsDirSep(*clean_start))
      return ON_wString_CleanPathFailed();
  }

  if (bDeleteWindowsUNCHostNameOrDiskLetter && (bIsUNCHostName || bIsWindowsDrive))
  {
    // Delete Windows UNC host name or drive letter
    local_dirty_path = clean_start;
    dirty_path = local_dirty_path;
    clean_path = dirty_path;
    clean_head = clean_path.Array();
    clean_start = clean_head;
  }

  const size_t clean_start_offset = (clean_start - clean_head);

  wchar_t* dst = clean_start;
  wchar_t* src = dst;
  for (;;)
  {
    wchar_t c;
    // advance to directory separator
    for (c = *src; false == IsDirSep(c) && 0 != c; c = *(++src))
    {
      *dst++ = c;
    }
    if ( 0 == c )
      break;
    // normalize directory separator
    *dst++ = (0 != directory_separator) ? directory_separator : c;
    // Condense /./ and //
    for (src++; (IsDirSep(*src) || IsDotDir(src)); src++)
    {
      // empty body;
    }
  }
  *dst = 0;
  if (dst > clean_head)
  {
    clean_path.SetLength(dst - clean_head);
    clean_head = clean_path.Array();
    clean_start = clean_head + clean_start_offset;
  }
  else
  {
    return ON_wString_CleanPathFailed();
  }

  dst = clean_start;
  if (IsDirSep(*dst))
  {
    // Skip over root directory separator
    dst++;
  }
  else
  {
    // Skip over initial ../../../ ... at start of a relative path
    while (IsDotDotDir(dst))
      dst += 3;
  }
  if ( 0 == *dst )
    return clean_path;

  src = dst;
  bool bDirty = false;
  while (*src)
  {
    if (IsDotDotDir(src))
    {
      if (!bDirty)
      {
        ON_ERROR("Bug.");
        return clean_path;
      }
      // will be dealt with in the recursive clean
      *dst++ = *src++;
      *dst++ = *src++;
      *dst++ = *src++;
      continue;
    }
    wchar_t* dir = dst;
    while (false == IsDirSep(*src) && 0 != *src)
    {
      *dst++ = *src++;
    }
    if (dir == dst)
    {
      ON_ERROR("Bug.");
      return clean_path;
    }
    if (0 == *src)
    {
      break;
    }
    if (IsDotDotDir(src + 1))
    {
      // replace dir/../ with ./ and recursively clean 
      dst = dir;
      dst[0] = '.';
      dst[1] = src[3];
      dst += 2;
      src += 4;
      bDirty = true;
      while (IsDotDotDir(src))
      {
        // will be dealt with in the recursive clean
        *dst++ = *src++;
        *dst++ = *src++;
        *dst++ = *src++;
      }
    }
    else if (IsDirSep(*src))
    {
      *dst++ = *src++;
    }
    else
    {
      ON_ERROR("Bug");
      return clean_path;
    }
  }

  if (dst > clean_head)
  {
    *dst = 0;
    clean_path.SetLength(dst - clean_head);
    clean_head = clean_path.Array();
    clean_start = clean_head + clean_start_offset;
  }
  else
  {
    ON_ERROR("Bug.");
    return clean_path;
  }

  if ( false == bDirty )
    return clean_path;

  if (dst >= src)
  {
    ON_ERROR("Bug.");
    return clean_path;
  }

  // recursively clean 
  const ON_wString clean_tail = ON_FileSystemPath::CleanPath(false,false,false,false,0,clean_start);
  if (clean_head < clean_start)
  {
    clean_path.SetLength(clean_start - clean_head);
    clean_path += clean_tail;
  }
  else
    clean_path = clean_tail;

  return clean_path;
}

const ON_wString ON_FileSystemPath::ExpandUser(
  const char* dirty_path
)
{
  const ON_wString dirty_local_path(dirty_path);
  return ON_FileSystemPath::ExpandUser(static_cast<const wchar_t*>(dirty_local_path));
}

const ON_wString ON_FileSystemPath::ExpandUser(
  const wchar_t* dirty_path
)
{
  for(;;)
  {
    if (nullptr == dirty_path)
      break;
    if (ON_wString::Tilde != dirty_path[0])
      break;
    if (false == ON_FileSystemPath::IsDirectorySeparator(dirty_path[1], true))
      break;
    ON_wString expanduser_path = ON_FileSystemPath::PlatformPath(ON_FileSystemPath::PathId::HomeDirectory);
    if (expanduser_path.IsEmpty())
      break;
    const wchar_t dir_seps[3]
      =
    {
      ON_FileSystemPath::DirectorySeparator,
      ON_FileSystemPath::AlternateDirectorySeparator,
      0
    };
    expanduser_path.TrimRight(dir_seps);
    if (expanduser_path.IsEmpty())
      break;
    expanduser_path += ON_wString(dirty_path + 1);
    return expanduser_path;
  }
  return ON_wString(dirty_path);
}

bool ON_FileSystem::PathExists(
  const char* path
  )
{
#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  return ::PathFileExistsA(path) ? true : false;
#else
  struct stat s;
  if (0 == stat(path, &s))
  {
    if (0 != (s.st_mode & (S_IFDIR|S_IFREG)))
      return true;
  }
  return false;
#endif
}

bool ON_FileSystem::PathExists(
  const wchar_t* path
  )
{
#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  return ::PathFileExistsW(path) ? true : false;
#else
  const ON_String pathUTF8(path);
  return ON_FileSystem::PathExists(static_cast<const char*>(pathUTF8));
#endif
}

bool ON_FileSystem::IsDirectory(
  const char* path
  )
{
  if (0 == path || 0 == path[0])
    return false;
#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  return ::PathIsDirectoryA(path) ? true : false;
#else
  struct stat s;
  if (0 == stat(path, &s))
  {
    if (0 != (s.st_mode & S_IFDIR))
      return true;
  }
  return false;
#endif
}

bool ON_FileSystem::IsDirectory(
  const wchar_t* path
  )
{
  if (0 == path || 0 == path[0])
    return false;
#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  return ::PathIsDirectoryW(path) ? true : false;
#else
  const ON_String pathUTF8(path);
  return ON_FileSystem::IsDirectory(static_cast<const char*>(pathUTF8));
#endif
}

bool ON_FileSystem::IsFile(
  const char* path
  )
{
  if (0 == path || 0 == path[0])
    return false;
#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  return ::PathFileExistsA(path) && 0 == ::PathIsDirectoryA(path);
#else
  struct stat s;
  if (0 == stat(path, &s))
  {
    if (0 == (s.st_mode & S_IFDIR) && 0 != (s.st_mode & S_IFREG))
      return true;
  }
  return false;
#endif
}

bool ON_FileSystem::IsFile(
  const wchar_t* path
  )
{
  if (0 == path || 0 == path[0])
    return false;
#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  return ::PathFileExistsW(path) && 0 == ::PathIsDirectoryW(path);
#else
  const ON_String pathUTF8(path);
  return ON_FileSystem::IsFile(static_cast<const char*>(pathUTF8));
#endif
}


bool ON_FileSystem::RemoveFile(
  const char* file_path
)
{
  for (;;)
  {
    if ( false == ON_FileSystem::IsFile(file_path) )
      break;
    int rc;
#if defined(ON_RUNTIME_WIN)
    rc = ::_unlink(file_path);
#elif defined(ON_RUNTIME_APPLE)
    rc = ::unlink(file_path);
#else
    rc = std::remove(file_path);
#endif
    if (0 == rc)
      return true;
    break;
  }

  return false;
}

bool ON_FileSystem::RemoveFile(
  const wchar_t* file_path
)
{
  for (;;)
  {
    if ( false == ON_FileSystem::IsFile(file_path) )
      break;
    int rc;
#if defined(ON_RUNTIME_WIN)
    rc = ::_wunlink(file_path);
#elif defined(ON_RUNTIME_APPLE)
    const ON_String utf8_file_path(file_path);
    rc = ::unlink(static_cast<const char*>(utf8_file_path));
#else
    const ON_String utf8_file_path(file_path);
    rc = std::remove(static_cast<const char*>(utf8_file_path));
#endif
    if (0 == rc)
      return true;
    break;
  }

  return false;
}

bool ON_FileSystem::IsDirectoryWithWriteAccess(
  const char* path
)
{
  const ON_wString wide_path(path);
  return ON_FileSystem::IsDirectoryWithWriteAccess(static_cast<const wchar_t*>(wide_path));
}

bool ON_FileSystem::IsDirectoryWithWriteAccess(
  const wchar_t* path
)
{
  if (false == ON_FileSystem::IsDirectory(path))
    return false;

  // native OS tools that query file systems do not
  // work on some network drives.
  // According to Microsoft techs, the only failsafe way
  // is to attempt to write a file.
  // https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/f57928d3-d89b-426d-a174-d06d97355afc/how-to-check-if-a-filefolder-is-writable-or-not?forum=windowssdk


  // try 2 uuids to get a file name that is not in use.
  for (int attempt = 0; attempt < 2; attempt++)
  {
    const ON_UUID id = ON_CreateId();
    wchar_t s[41];
    memset(s, 0, sizeof(s));
    ON_UuidToString(id, s);
    s[36] = '.';
    s[37] = 't';
    s[38] = 'm';
    s[39] = 'p';
    s[40] = 0;
    const ON_wString tmpfilename = ON_FileSystemPath::CombinePaths(
      path, false, s, true, false
    );
    if (ON_FileSystem::PathExists(tmpfilename))
      continue;

    FILE* fp = ON_FileStream::Open(tmpfilename, L"wb");
    if (nullptr == fp)
      break; // cannot open a file in path
    bool bIsDirectoryWithWriteAccess = false;
    for ( ;; )
    {
      char c = 0;
      const ON__UINT64 sizeof_c = sizeof(c);
      const ON__UINT64 count = ON_FileStream::Write(fp, sizeof_c, &c);
      if (0 != ON_FileStream::Close(fp))
        break; // cannot close the file.
      fp = nullptr;
      if (count != sizeof_c)
        break; // cannot write to the file in path
      bIsDirectoryWithWriteAccess = ON_FileSystem::PathExists(tmpfilename);
      // The purpose of this function is to test if a file can be opened and
      // written to using the same tools that write .3dm files.
      //
      // It is possible to have create and write permissions but not have 
      // read permissions. For this reason, we do not attempt to read the tmp file.
      break;
    }
    if (nullptr == fp)
    {
      // The purpose of this function is to test if a file can be opened and
      // written to using the same tools that write .3dm files.
      //
      // There is speculation that when a directory is managed by dropbox 
      // or perhaps other network storage devices, there may be significant 
      // latency in the file systems that results in a time lag between calling 
      // unlink() and having ON_FileSystem::IsFile() report false.
      // For that reason, we do not check success codes on unlink 
      // or verify the tmp file is gone.
      ON_FileSystem::RemoveFile(tmpfilename);
    }
    return bIsDirectoryWithWriteAccess;
  }

  return false;
}

const ON_wString ON_FileSystemPath::FullPathFromRelativePath(
  const wchar_t* base_path,
  bool bBasePathIncludesFileName,
  const wchar_t* relative_path
  )
{
  if ( nullptr == relative_path || 0 == relative_path )
    return ON_wString::EmptyString;

  if ( nullptr == base_path || 0 == base_path[0] )
    return ON_wString::EmptyString;

  const wchar_t* base_path_end = nullptr;
  if (bBasePathIncludesFileName)
  {
    on_wsplitpath(base_path, nullptr, nullptr, &base_path_end, nullptr);
  }
  else
  {
    base_path_end = base_path + ON_wString::Length(base_path);
  }
  if (nullptr == base_path_end)
    return ON_wString::EmptyString;
  if (!(base_path < base_path_end))
    return ON_wString::EmptyString;
  
  ON_wString dirty_full_path;
  dirty_full_path.Append(base_path,(int)(base_path_end - base_path));
  if ( false == ON_FileSystemPath::IsDirectorySeparator(base_path_end[-1],true) )
    dirty_full_path += ON_FileSystemPath::DirectorySeparator;
  dirty_full_path += relative_path;
  return ON_FileSystemPath::CleanPath(dirty_full_path);
}

static bool CleanAndRemoveFileName(
  const wchar_t* dirty_path,
  bool bPathIncludesFileName,
  ON_wString& volume,
  ON_wString& clean_path,
  ON_wString* file_name
  )
{
  ON_wString path = ON_FileSystemPath::CleanPath(dirty_path);
  for (;;)
  {
    if (path.IsEmpty())
      break;

    if (false == bPathIncludesFileName && false == IsDirSep(path[path.Length() - 1]))
      path += ON_FileSystemPath::DirectorySeparator;

    const wchar_t* p = static_cast<const wchar_t*>(path);
    const wchar_t* v = nullptr;
    const wchar_t* d = nullptr;
    const wchar_t* f = nullptr;
    on_wsplitpath(p, &v, &d, bPathIncludesFileName ? &f : nullptr, nullptr);

    if (nullptr == d || 0 == d[0])
      break;
    
    clean_path = d;

    if (bPathIncludesFileName)
    {
      // remove trailing file name from base_path.
      if (nullptr == f || 0 == f[0])
        break;
      const size_t path_length = (size_t)path.Length();
      if  ( path_length <= 0 )
        break;
      if (!(p <= d && d < f && f < p + path_length))
        break;
      if (!IsDirSep(f[-1]))
        break;
      size_t len = (f - d);
      if (len <= 1 || len >= (size_t)clean_path.Length())
        break;
      if ( nullptr != file_name )
        *file_name = f;
      clean_path.SetLength(len);
    }
    else
    {
      if ( nullptr != file_name )
        *file_name = ON_wString::EmptyString;
    }

    return true;
  }

  volume = ON_wString::EmptyString;
  clean_path = ON_wString::EmptyString;
  if ( nullptr != file_name )
    *file_name = ON_wString::EmptyString;
  return false;
}

const ON_wString ON_FileSystemPath::RelativePath(
  const wchar_t* full_path,
  bool bFullPathIncludesFileName,
  const wchar_t* base_path,
  bool bBasePathIncludesFileName
  )
{
  ON_wString best_answer(full_path);

  ON_wString full_volume;
  ON_wString local_full;
  ON_wString file_name;
  if (false == CleanAndRemoveFileName(full_path,bFullPathIncludesFileName,full_volume,local_full,&file_name))
    return best_answer;

  best_answer = local_full;
  best_answer += file_name;

  ON_wString base_volume;
  ON_wString local_base;
  if (false == CleanAndRemoveFileName(base_path,bBasePathIncludesFileName,base_volume,local_base,nullptr))
    return best_answer;

  if (full_volume.IsNotEmpty() || base_volume.IsNotEmpty() )
  {
    if (false == ON_wString::EqualPath(full_volume,base_volume))
      return best_answer;
  }

  const wchar_t* full_tail = static_cast<const wchar_t*>(local_full);
  const wchar_t* base_tail = static_cast<const wchar_t*>(local_base);
  if (false == IsDirSep(*full_tail) || false == IsDirSep(*base_tail))
  {
    // A double directory separator after the initial CleanAndRemoveFileName()
    // calls indicates invalid file path informtion.
    return best_answer;
  }

  // Skip initial directory separator
  full_tail++;
  base_tail++;
  if (0 == *full_tail || 0 == *base_tail)
  {
    return best_answer;
  }
  if (IsDirSep(*full_tail) || IsDirSep(*base_tail))
  {
    // A double directory separator after the initial ON_FileSystemPath::CleanPath()
    // calls indicates invalid file path informtion.
    return best_answer;
  }

  int overlap_directory_count = 0;
  if (0 != *full_tail && 0 != *base_tail)
  {
    const wchar_t* full1 = full_tail;
    const wchar_t* base1 = base_tail;
    while (0 != *full1 && 0 != *base1 )
    {
      if (IsDotDir(base1) || IsDotDotDir(base1))
      {
        overlap_directory_count = 0;
        break;
      }
      bool bFullDirSep = IsDirSep(*full1);
      bool bBaseDirSep = IsDirSep(*base1);
      if (false == bFullDirSep && false == bBaseDirSep)
      {
        // skipping an element of a directory name 
        base1++;
        full1++;
        continue;
      }
      if (bFullDirSep && bBaseDirSep)
      {
        if (false == ON_wString::EqualPath(full_tail, (int)(full1 - full_tail), base_tail, (int)(base1 - base_tail)))
        {
         // directory names have identical lengths and different content
          break;
        }

        // matching directory names

        // skip directory separator
        base1++;
        full1++;
        if (IsDirSep(*base1) || IsDirSep(*full1))
        {
          // damaged input
          break;
        }
        base_tail = base1;
        full_tail = full1;
        overlap_directory_count++;
        continue;
      }
      // directory names have different lengths
      break;
    }
  }

  if (overlap_directory_count < 1)
    return best_answer;

  // It is reasonable for base_tail to be nullptr
  if (nullptr == full_tail && IsDirSep(*full_tail) )
    return best_answer;

  // It is reasonable for base_tail to be nullptr
  if (nullptr != base_tail && IsDirSep(*base_tail) )
    return best_answer;
  
  // set dotdot_count to number of directories left in base_tail
  int dotdot_count = 0;
  while (0 != *base_tail)
  {
    if (IsDotDir(base_tail) || IsDotDotDir(base_tail))
      return best_answer;
    if (IsDirSep(*base_tail))
    {
      if (IsDirSep(base_tail[1]))
        return best_answer; // shouldn't be double dir seps after the initial clean
      dotdot_count++;
    }
    base_tail++;
  }

  // buid relative path
  ON_wString relative_path;
  if (0 == dotdot_count)
  {
    relative_path = L".";
    relative_path += ON_FileSystemPath::DirectorySeparator;
  }
  else
  {
    for (int i = 0; i < dotdot_count; i++)
    {
      relative_path += L"..";
      relative_path += ON_FileSystemPath::DirectorySeparator;
    }
  }
  if (nullptr != full_tail && 0 != full_tail[0] )
    relative_path += full_tail;
  if (file_name.IsNotEmpty())
    relative_path += file_name;

  return relative_path;
}


//////////////////////////////////////////////////////////////////////////////
//
// ON_FileStream implementation
//

FILE* ON_FileStream::Open( const wchar_t* filename, const wchar_t* mode )
{
  FILE* fp = 0;

  if ( 0 == filename || 0 == filename[0] || 0 == mode || 0 == mode[0] )
    return fp;

#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  errno_t e = _wfopen_s(&fp,filename,mode); 
  if ( 0 != e && 0 == fp )
    fp = 0; // reference e to keep lint quiet.
#else
  // I can't find an wfopen() or _wfopen() in
  // gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)
  ON_String fnameUTF8(filename);
  ON_String modeUTF8(mode);
  fp = fopen(fnameUTF8,modeUTF8);
#endif

  return fp;
}

FILE* ON_FileStream::Open( const char* filename, const char* mode )
{
  FILE* fp = 0;

  if ( 0 == filename || 0 == filename[0] || 0 == mode || 0 == mode[0] )
    return fp;

#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  errno_t e = fopen_s(&fp,filename,mode); 
  if ( 0 != e && 0 == fp )
    fp = 0; // reference e to keep lint quiet.
#else
  fp = fopen(filename,mode);
#endif

  return fp;
}

int ON_FileStream::Close( FILE* fp )
{
  return ( ( 0 != fp ) ? fclose(fp) : -1 );
}

bool ON_FileStream::Is3dmFile(
  const wchar_t* file_path,
  bool bAllow3dmbakExtension
)
{
  for (;;)
  {
    if (false == ON_FileSystemPath::FilePathHas3dmExtension(file_path, bAllow3dmbakExtension))
      break;
    FILE* fp = ON_FileStream::Open3dmToRead(file_path);
    if (nullptr == fp)
      break;
    ON_FileStream::Close(fp);
    return true;
  }
  return false;
}

bool ON_FileStream::Is3dmFile(
  const char* file_path,
  bool bAllow3dmbakExtension
)
{
  for (;;)
  {
    if (false == ON_FileSystemPath::FilePathHas3dmExtension(file_path, bAllow3dmbakExtension))
      break;
    FILE* fp = ON_FileStream::Open3dmToRead(file_path);
    if (nullptr == fp)
      break;
    ON_FileStream::Close(fp);
    return true;
  }
  return false;
}

bool ON_FileSystemPath::FilePathHas3dmExtension(
  const wchar_t* file_path,
  bool bAllow3dmbakExtension
)
{
  for (;;)
  {
    // test file name
    const wchar_t* e = nullptr;
    on_wsplitpath(file_path, nullptr, nullptr, nullptr, &e);
    if (nullptr == e)
      break;
    if ('.' != e[0])
      break;
    if ('3' != e[1])
      break;
    if ('d' != e[2] && 'D' != e[2])
      break;
    if ('m' != e[3] && 'M' != e[3])
      break;
    if (0 == e[4])
      return true;
    if (false == bAllow3dmbakExtension)
      break;
    if ('b' != e[4] && 'B' != e[4])
      break;
    if ('a' != e[5] && 'A' != e[5])
      break;
    if ('k' != e[6] && 'K' != e[6])
      break;
    if (0 != e[7])
      break;
    return true;
  }
  return false;
}

bool ON_FileSystemPath::FilePathHas3dmExtension(
  const char* file_path,
  bool bAllow3dmbakExtension
)
{
  for (;;)
  {
    // test file name
    const char* e = nullptr;
    on_splitpath(file_path, nullptr, nullptr, nullptr, &e);
    if (nullptr == e)
      break;
    if ('.' != e[0])
      break;
    if ('3' != e[1])
      break;
    if ('d' != e[2] && 'D' != e[2])
      break;
    if ('m' != e[3] && 'M' != e[3])
      break;
    if (0 == e[4])
      return true;
    if (false == bAllow3dmbakExtension)
      break;
    if ('b' != e[4] && 'B' != e[4])
      break;
    if ('a' != e[5] && 'A' != e[5])
      break;
    if ('k' != e[6] && 'K' != e[6])
      break;
    if (0 != e[7])
      break;
    return true;
  }
  return false;
}

bool ON_FileSystemPath::IsValidFileName(
  const char* file_name,
  bool bAllPlatforms
)
{
  const ON_wString wide_file_name(file_name);
  return ON_FileSystemPath::IsValidFileName(static_cast<const wchar_t*>(wide_file_name), bAllPlatforms);
}

bool ON_FileSystemPath::IsValidFileName(
  const wchar_t* file_name,
  bool bAllPlatforms
)
{
  if (nullptr == file_name || 0 == file_name[0])
    return false;

  bool bDoubleDot = false;
  wchar_t prev_c = 0;
  int file_name_length;
  for (file_name_length = 0; 0 != file_name[file_name_length]; file_name_length++)
  {
    if (file_name_length > 256)
      return false;

    // note that all illegal symbols currently tested for have 
    // UNICODE code points <= U+07F, so we can simply test c
    const wchar_t c = file_name[file_name_length];
    if (ON_FileSystemPath::IsDirectorySeparator(c, bAllPlatforms))
      return false;

    switch (c)
    {
    case '.':
      bDoubleDot = ('.' == prev_c);
      break;

    case ':':
#if defined(ON_RUNTIME_WIN)
      return false;
#else
      if (bAllPlatforms)
        return false;
#endif
      break;

      //// Most windows apps have these restrictions, but the file system supports 
      //// names with these characters.
      ////    case ':':
      ////    case '~':
      ////    case '#':
      ////    case '%':
      ////    case '&':
      ////    case '*':
      ////    case '{':
      ////    case '}':
      ////    case '<':
      ////    case '>':
      ////    case '?':
      ////    case '|':
      ////    case '"':
      ////#if defined(ON_RUNTIME_WIN)
      ////      return false;
      ////#else
      ////      if (bAllPlatforms)
      ////        return false;
      ////#endif
      ////    
    }

    prev_c = c;
  }

  switch(prev_c)
  {
  case '.':
    if (1 == file_name_length)
      return false;
    if (2 == file_name_length && bDoubleDot)
      return false; 
    break;

  case '~':
    if (1 == file_name_length)
      return false;
    break;
  }

  return true;
}

const ON_wString ON_FileSystemPath::PlatformPath(ON_FileSystemPath::PathId path_id)
{
#if defined(ON_RUNTIME_WIN)
  KNOWNFOLDERID platform_path_id;
#define ON_INTERNAL_SET_LOCAL_DIRECTORY_ID(win_fid,apple_fid) platform_path_id = win_fid
#elif defined(ON_RUNTIME_APPLE_OBJECTIVE_C_AVAILABLE)
  NSSearchPathDirectory platform_path_id;
#define ON_INTERNAL_SET_LOCAL_DIRECTORY_ID(win_fid,apple_fid) platform_path_id = apple_fid
#endif

#if defined(ON_INTERNAL_SET_LOCAL_DIRECTORY_ID)
  switch (path_id)
  {
  case ON_FileSystemPath::PathId::DesktopDirectory:
    ON_INTERNAL_SET_LOCAL_DIRECTORY_ID(FOLDERID_Desktop, NSDesktopDirectory);
    break;
  case ON_FileSystemPath::PathId::DocumentsDirectory:
    ON_INTERNAL_SET_LOCAL_DIRECTORY_ID(FOLDERID_Documents, NSDocumentDirectory);
    break;
  case ON_FileSystemPath::PathId::DownloadsDirectory:
    ON_INTERNAL_SET_LOCAL_DIRECTORY_ID(FOLDERID_Downloads, NSDownloadsDirectory);
    break;
  case ON_FileSystemPath::PathId::HomeDirectory:
//#if defined(ON_RUNTIME_WIN)
//    platform_path_id = FOLDERID_Profile;
//#elif defined(ON_RUNTIME_APPLE_OBJECTIVE_C_AVAILABLE)
//     platform_path_id = NSUserDirectory;
//#endif
    ON_INTERNAL_SET_LOCAL_DIRECTORY_ID(FOLDERID_Profile, NSUserDirectory);
    //ON_INTERNAL_SET_LOCAL_DIRECTORY_ID(FOLDERID_Profile, NSHomeDirectory);
    break;
  default:
    return ON_wString::EmptyString;
  }
#undef ON_INTERNAL_SET_LOCAL_DIRECTORY_ID
#endif

  ON_wString path;

#if defined(ON_RUNTIME_WIN)

  const DWORD dwFlags = KF_FLAG_DEFAULT;
  const HANDLE hToken = nullptr; // current user
  wchar_t* windows_path = nullptr;
  const HRESULT hr = ::SHGetKnownFolderPath(
    platform_path_id,
    dwFlags,
    hToken,
    &windows_path
  );
  if (nullptr != windows_path)
  {
    if (S_OK == hr)
      path = windows_path;
    CoTaskMemFree(windows_path);
  }

#elif defined(ON_RUNTIME_APPLE_OBJECTIVE_C_AVAILABLE)

  NSArray *apple_paths = NSSearchPathForDirectoriesInDomains(platform_path_id, NSUserDomainMask, YES);
  if ([apple_paths count] > 0)
  {
    NSString* apple_path = [apple_paths objectAtIndex : 0];
    if (nullptr != apple_path)
    {
      ON_wString s;
      const int len = (int)apple_path.length;
      s.SetLength(len);
      int idx;
      for (idx = 0; idx < len; idx++)
        s[idx] = [apple_path characterAtIndex : idx];
      s[idx] = 0;
      path = s;
    }
  }

#else

  if (ON_FileSystemPath::PathId::HomeDirectory != path_id)
  {
    ON_ERROR("Function not implemented.");
  }

#endif

  path.TrimLeftAndRight();

  // See if environment variables will
  if (ON_FileSystemPath::PathId::HomeDirectory == path_id && path.IsEmpty())
  {
    const wchar_t dir_seps[4] = {
      ON_FileSystemPath::DirectorySeparator,
      ON_FileSystemPath::AlternateDirectorySeparator,
      ON_wString::Space,
      0
    };
    for (;;)
    {
      path = ON_wString(getenv("HOME"));
      path.TrimLeftAndRight();
      path.TrimRight(dir_seps);
      if ( ON_FileSystem::IsDirectory(path) )
        break;

#if defined(ON_RUNTIME_WIN)
      path = ON_wString(getenv("USERPROFILE"));
      path.TrimLeftAndRight();
      path.TrimRight(dir_seps);
      if ( ON_FileSystem::IsDirectory(path) )
        break;

      path = ON_wString(getenv("HOMEDRIVE")) + ON_wString(getenv("HOMEPATH"));
      path.TrimLeftAndRight();
      path.TrimRight(dir_seps);
      if ( ON_FileSystem::IsDirectory(path) )
        break;
#endif
      path = ON_wString::EmptyString;
      break;
    }
  }

  return path;
}

static unsigned int ON_Internal_SeekTo3DGeometryFileFormatMark(
  FILE* fp
)
{
  const char* tag = "3D Geometry File Format ";
  char buffer[33] = {};

  for (;;)
  {
    if (32 != ON_FileStream::Read(fp, 32, buffer))
      break;

    if (0 != ON_String::CompareOrdinal(tag, 24, buffer, 24, false))
    {
      // it's not a "pure" .3DM file 
      // - see if we have a .3DM file with MS OLE-goo at the start
      // (generally, there is around 6kb of goo.  I keep looking
      // for up to 32mb just in case.)
      unsigned int offset = 0;
      for (unsigned int n = 0; n < 33554432; n++)
      {
        for (int j = 0; j < 31; j++)
          buffer[j] = buffer[j + 1];
        if (!ON_FileStream::Read(fp, 1, &buffer[31]))
          break;
        if (0 == ON_String::CompareOrdinal(tag, 24, buffer, 24, false))
        {
          offset = n + 1;
          break;
        }
      }
      if (0 == offset)
        break;
    }

    // get version
    //char* sVersion = s3d+24;
    // skip leading spaces
    int ver = 0;
    int i = 24;
    while (i < 32 && buffer[i] == ' ')
      i++;
    while (i < 32)
    {
      // TEMPORARY 2 = X
      if (i == 31 && buffer[i] == 'X')
      {
        buffer[i] = '2';
      }

      if (buffer[i] < '0' || buffer[i] > '9')
      {
        // it's not a valid .3DM file version
        break;
      }
      ver = ver * 10 + ((int)(buffer[i] - '0'));
      i++;
    }
    if (ver <= 0)
      break;

    if (false == ON_FileStream::SeekFromCurrentPosition(fp, -32))
      break;

    return (unsigned int)ver;
  }
  ON_FileStream::SeekFromStart(fp, 0);
  return false;
}

FILE* ON_FileStream::Open3dmToRead(
  const wchar_t* file_path
)
{
  FILE* fp = nullptr;

  for (;;)
  {
    fp = ON_FileStream::Open(file_path,L"rb");
    if (nullptr == fp)
      break;

    if ( 0 == ON_Internal_SeekTo3DGeometryFileFormatMark(fp))
      break;

    return fp;
  }

  if ( nullptr != fp )
    ON::CloseFile(fp);

  return nullptr;
}

FILE* ON_FileStream::ON_FileStream::Open3dmToRead(
  const char* file_path
)
{
  FILE* fp = nullptr;

  for (;;)
  {
    fp = ON_FileStream::Open(file_path,"rb");
    if (nullptr == fp)
      break;

    if ( 0 == ON_Internal_SeekTo3DGeometryFileFormatMark(fp))
      break;

    return fp;
  }

  if ( nullptr != fp )
    ON::CloseFile(fp);

  return nullptr;
}


ON__INT64 ON_FileStream::CurrentPosition( FILE* fp )
{
  if ( 0 == fp )
    return -1;
#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  return _ftelli64(fp);
#else
  return ftell(fp);
#endif
}

bool ON_FileStream::SeekFromCurrentPosition( FILE* fp, ON__INT64 offset )
{
  return ON_FileStream::Seek(fp,offset,SEEK_CUR);
}

bool ON_FileStream::SeekFromStart( FILE* fp, ON__INT64 offset )
{
  return ON_FileStream::Seek(fp,offset,SEEK_SET);
}

bool ON_FileStream::SeekFromEnd( FILE* fp, ON__INT64 offset )
{
  return ON_FileStream::Seek(fp,offset,SEEK_END);
}

bool ON_FileStream::Seek( FILE* fp, ON__INT64 offset, int origin )
{
  if ( 0 == fp )
    return false;

  if ( origin < 0 || origin > 2 )
    return false;

  if ( 0 == offset && SEEK_CUR == origin )
    return true;
  
#if defined(ON_COMPILER_MSC) && defined(ON_RUNTIME_WIN)
  if (0 != _fseeki64(fp, offset, origin))
    return false;
#else

  const int i = 2147483646;
  const ON__INT64 i64 = i;

  while ( offset > i64 )
  {
    if ( 0 != fseek( fp, i, origin ) )
      return false;
    if (SEEK_CUR != origin)
      origin = SEEK_CUR;
    offset -= i64;
  }

  while ( offset < -i64 )
  {
    if ( 0 != fseek( fp, -i, origin ) )
      return false;
    if (SEEK_CUR != origin)
      origin = SEEK_CUR;
    offset += i64;
  }

  if (0 != offset || SEEK_CUR != origin)
  {
    int ioffset = (int)offset;
    if (0 != fseek(fp, ioffset, origin))
      return false;
  }

#endif

  return true;
}

ON__UINT64 ON_FileStream::Read( FILE* fp, ON__UINT64 count, void* buffer )
{
  ON__UINT64 rc = 0;
  if ( 0 == fp || count <= 0 || 0 == buffer )
    return rc;

  if ( count <= ON_MAX_SIZE_T )
  {
    rc = (ON__UINT64)fread(buffer,1,(size_t)count,fp);
  }
  else
  {
    size_t sz, szread;
    while ( count > 0 )
    {
      sz = ( count > ON_MAX_SIZE_T ) ? ON_MAX_SIZE_T : ((size_t)count);
      szread = fread(buffer,1,sz,fp);
      rc += szread;
      if ( szread != sz )
        break;
      count -= sz;
      buffer = ((unsigned char*)buffer) + sz;
    }
  }

 return rc;
}

ON__UINT64 ON_FileStream::Write( FILE* fp, ON__UINT64 count, const void* buffer )
{
  ON__UINT64 rc = 0;
  if ( 0 == fp || count <= 0 || 0 == buffer )
    return rc;

  if ( count <= ON_MAX_SIZE_T )
  {
    rc = fwrite(buffer,1,(size_t)count,fp);
  }
  else
  {
    size_t sz, szwrite;
    while ( count > 0 )
    {
      sz = ( count > ON_MAX_SIZE_T ) ? ON_MAX_SIZE_T : ((size_t)count);
      szwrite = fwrite(buffer,1,sz,fp);
      rc += szwrite;
      if ( szwrite != sz )
        break;
      count -= sz;
      buffer = ((unsigned char*)buffer) + sz;
    }
  }

 return rc;
}

bool ON_FileStream::Flush( FILE* fp )
{
  if ( 0 == fp )
    return false;
  if ( 0 != fflush(fp) )
    return false;
  return true;
}

bool ON_FileStream::GetFileInformation(
  const wchar_t* file_name,
  ON__UINT64* file_size,
  ON__UINT64* file_metadata_last_modified_time,
  ON__UINT64* file_contents_last_modified_time
  )
{
  FILE* fp = ON::OpenFile(file_name, L"rb");
  bool rc = ON_FileStream::GetFileInformation(fp,file_size,file_metadata_last_modified_time,file_contents_last_modified_time);
  ON::CloseFile(fp);
  return rc;
}

bool ON_FileStream::GetFileInformation(
  const char* file_name,
  ON__UINT64* file_size,
  ON__UINT64* file_metadata_last_modified_time,
  ON__UINT64* file_contents_last_modified_time
  )
{
  FILE* fp = ON::OpenFile(file_name, "rb");
  bool rc = ON_FileStream::GetFileInformation(fp,file_size,file_metadata_last_modified_time,file_contents_last_modified_time);
  ON::CloseFile(fp);
  return rc;
}

bool ON_FileStream::GetFileInformation( 
    FILE* fp,
    ON__UINT64* file_size,
    ON__UINT64* file_metadata_last_modified_time,
    ON__UINT64* file_contents_last_modified_time
    )
{
  bool rc = false;

  if (file_size)
    *file_size = 0;
  if (file_metadata_last_modified_time)
    *file_metadata_last_modified_time = 0;
  if (file_contents_last_modified_time)
    *file_contents_last_modified_time = 0;

  if ( fp )
  {

#if defined(ON_COMPILER_MSC)

    // Microsoft compilers
#if (_MSC_VER >= 1400)
    // VC 8 (2005) 
    // works for file sizes > 4GB 
    // when size_t is a 64 bit integer
    struct _stat64 sb;
    memset(&sb,0,sizeof(sb));
    int fd = _fileno(fp);    
    int fstat_rc = _fstat64(fd, &sb);
#else
    // VC6 compiler
    struct _stat sb;
    memset(&sb,0,sizeof(sb));
    int fd = _fileno(fp);    
    int fstat_rc = _fstat(fd, &sb);
#endif

#else
    // works on most compilers
    int fd = fileno(fp);
    struct stat sb;
    memset(&sb,0,sizeof(sb));
    int fstat_rc = fstat(fd, &sb);
#endif


    if (0 == fstat_rc)
    {
      if (file_size)
        *file_size = (ON__UINT64)sb.st_size;
      if (file_metadata_last_modified_time)
        *file_metadata_last_modified_time = (ON__UINT64)sb.st_ctime;
      if (file_contents_last_modified_time)
        *file_contents_last_modified_time = (ON__UINT64)sb.st_mtime;
      rc = true;
    }
  }

  return rc;
}

//////////////////////////////////////////////////////////////////////////////
//
// ON_FileIterator implementation
//

class ON_DirectoryIteratorImpl
{
private:
  friend class ON_FileIterator;

  ON_DirectoryIteratorImpl();
  ~ON_DirectoryIteratorImpl();
  ON_DirectoryIteratorImpl(const ON_DirectoryIteratorImpl&) = delete;
  ON_DirectoryIteratorImpl& operator=(const ON_DirectoryIteratorImpl&) = delete;

  const wchar_t* CurrentFileNameAsPointer() const;

#if defined(ON_COMPILER_MSC)
  // Used by Windows ::Find
  //ON__UINT32 m_file_attributes_mask = 0;
  HANDLE m_h = 0;
  WIN32_FIND_DATA m_fd;
#else
  // Apple and gcc
  ON_wString m_ws_file_name_filter;
  ON_String m_utf8_file_name_filter;
  DIR* m_dir = nullptr;
  struct dirent m_dirent;
  char m_dirent_name_buffer[NAME_MAX+1]; // < this field provide storage for m_dirent.d_name[]

  // information about the current file
  wchar_t m_current_name[1024];
  ON__UINT64 m_current_file_attributes = 0; // 1 = regular file, 2 = directory
  ON__UINT64 m_current_file_size = 0;
  ON__UINT64 m_current_content_last_modified_time = 0;
#endif
};

ON_DirectoryIteratorImpl::ON_DirectoryIteratorImpl()
{
#if defined(ON_COMPILER_MSC)
  memset(&m_fd,0,sizeof(m_fd));
#else
  memset(&m_dirent,0,sizeof(m_dirent));
  memset(&m_dirent_name_buffer[0],0,sizeof(m_dirent_name_buffer));
  memset(&m_current_name[0],0,sizeof(m_current_name));
#endif
}

ON_DirectoryIteratorImpl::~ON_DirectoryIteratorImpl()
{
#if defined(ON_COMPILER_MSC)
  if ( 0 != m_h )
    ::FindClose(m_h);
#else
  if ( 0 != m_dir )
    closedir(m_dir);
#endif
}

void ON_FileIterator::Reset()
{
  m_state = 0;
  m_directory = ON_wString::EmptyString;
  m_item_name_filter = ON_wString::EmptyString;
  m_item_name = ON_wString::EmptyString;
  m_full_path_name = ON_wString::EmptyString;
  m_count = 0;
  if (nullptr != m_impl)
  {
    delete m_impl;
    m_impl = nullptr;
  }
}


ON__UINT64 ON_FileIterator::CurrentItemCount() const
{
  return m_count;
}

#if defined(ON_COMPILER_MSC)
static bool IsDotOrDotDotDir( const wchar_t* s )
#else
static bool IsDotOrDotDotDir( const char* s )
#endif
{
  bool rc = false;
  for (;;)
  {
    if ( 0 == s )
      break;
    if ( '.' != s[0] )
      break;
    if ( 0 != s[1] )
    {
      if ( '.' != s[1] )
        break;
      if ( 0 != s[2] )
        break;
    }
    rc = true; // s = "." or s = ".."
    break;
  }
  return rc;
}

bool ON_FileIterator::Initialize( 
  const wchar_t* directory_name
  )
{
  const wchar_t* item_name_filter = nullptr;
  return Initialize(directory_name,item_name_filter);
}

bool ON_FileIterator::Initialize( 
  const wchar_t* directory_name, 
  const wchar_t* item_name_filter
  )
{
  const ON_wString local_item_name_filter(item_name_filter);
  item_name_filter = local_item_name_filter;

  ON_wString local_directory_name(directory_name);
  {
    const wchar_t* dir_seps = L"/\\";
    local_directory_name.TrimRight(dir_seps);
    if ( local_directory_name.Length() <= 0 || local_directory_name.IsEmpty() )
      local_directory_name = directory_name;
  }

  Reset();
  m_directory = local_directory_name;
  m_item_name_filter = local_item_name_filter;
  if (m_directory.IsEmpty())
    return false;
  m_state = 1;
  return true;
}

bool ON_FileIterator::Initialize( 
  const char* directory_name
  )
{
  const char* item_name_filter = nullptr;
  return Initialize(directory_name,item_name_filter);
}

bool ON_FileIterator::Initialize(
  const char* directory_name,
  const char* item_name_filter
  )
{
  const ON_wString local_directory_name(directory_name);
  const ON_wString local_item_name_filter(item_name_filter);
  return Initialize(
    static_cast<const wchar_t*>(local_directory_name),
    static_cast<const wchar_t*>(local_item_name_filter)
    );  
}

bool ON_FileIterator::FirstItem()
{
  const ON_wString saved_directory(m_directory);
  const ON_wString saved_item_name_filter(m_item_name_filter);
  if (m_state > 1)
  {
    Reset();
    m_directory = saved_directory;
    m_item_name_filter = saved_item_name_filter;
    if (saved_directory.IsEmpty())
      return false;
    m_state = 1;
  }

  if ( 1 != m_state  || nullptr != m_impl )
    return false;

  m_impl = new ON_DirectoryIteratorImpl();
  m_state = 2;

  const wchar_t* item_name_filter = static_cast<const wchar_t*>(m_item_name_filter);
  if ( nullptr != item_name_filter && 0 == item_name_filter[0] )
    item_name_filter = nullptr;

#if defined(ON_COMPILER_MSC)
  for (;;)
  {
    ON_wString s(m_directory);

    if (0 == item_name_filter)
    {
      // A null file file_name_filter means iterate 
      // through all items in the directory.  To do
      // this using Windows' ::FindFirstFile, set the
      // filter to "*.*", even though some items will
      // not contain a "dot".
      item_name_filter = L"*.*";
    }

    if (0 != item_name_filter[0] && s.IsNotEmpty())
    {
      s += ON_FileSystemPath::DirectorySeparator;
      s += item_name_filter;
    }

    m_impl->m_h = ::FindFirstFile(static_cast<const wchar_t*>(s), &m_impl->m_fd);
    if (0 == m_impl->m_h || INVALID_HANDLE_VALUE == m_impl->m_h || 0 == m_impl->m_fd.cFileName[0])
    {
      // Happens on "fake" directories like "My Music" and "My Videos"
      m_impl->m_h = 0;
      break;
    }

    if (IsDotOrDotDotDir(m_impl->m_fd.cFileName))
    {
      return NextItem();
    }

    m_count++;
    m_impl->m_fd.cFileName[(sizeof(m_impl->m_fd.cFileName) / sizeof(m_impl->m_fd.cFileName[0])) - 1] = 0;
    m_item_name = m_impl->m_fd.cFileName;
    m_full_path_name = ON_wString::EmptyString;
    return true;
  }

#else
  // gcc code
  m_impl->m_utf8_file_name_filter = item_name_filter;
  const ON_String utf8_str(m_directory); // convert wchar_t to utf8 string
  const char* s = utf8_str;
  m_impl->m_dir = (0 != s && 0 != s[0]) ? opendir(s) : 0;
  if ( 0 != m_impl->m_dir )
  {
    return NextItem();
  }
#endif

  Reset();
  m_directory = saved_directory;
  m_item_name_filter = saved_item_name_filter;
  m_state = 3;
  return false;
}

bool ON_FileIterator::NextItem()
{
  m_item_name = ON_wString::EmptyString;
  m_full_path_name = ON_wString::EmptyString;
  if ( 1 == m_state )
    return FirstItem();
  if ( 2 != m_state )
    return false;

#if defined(ON_COMPILER_MSC)
  for (;;)
  {
    if (0 == m_impl->m_h || INVALID_HANDLE_VALUE == m_impl->m_h || 0 == m_impl->m_fd.cFileName[0])
    {
      break;
    }

    for (;;)
    {
      if (!::FindNextFile(m_impl->m_h, &m_impl->m_fd) || 0 == m_impl->m_fd.cFileName[0])
        break;

      if (IsDotOrDotDotDir(m_impl->m_fd.cFileName))
        continue;

      m_count++;
      m_impl->m_fd.cFileName[(sizeof(m_impl->m_fd.cFileName) / sizeof(m_impl->m_fd.cFileName[0])) - 1] = 0;
      m_item_name = m_impl->m_fd.cFileName;
      m_full_path_name = ON_wString::EmptyString;
      return true;
    }

    break;
  }
#else

  // gcc code
  ON__UINT64 current_file_attributes = 0;
  wchar_t current_name[ sizeof(m_impl->m_current_name)/sizeof(m_impl->m_current_name[0]) ];
  for(;;)
  {
    current_file_attributes = 0;
    struct dirent* dp = 0;
    int readdir_errno = readdir_r(m_impl->m_dir, &m_impl->m_dirent, &dp);
    if ( 0 !=  readdir_errno )
      break;
    if ( 0 == dp )
      break;
    if ( 0 == m_impl->m_dirent.d_name[0] )
      break;

    if ( IsDotOrDotDotDir(m_impl->m_dirent.d_name) )
      continue;

    memset( current_name, 0, sizeof(current_name) );
    ON_ConvertUTF8ToWideChar(
      false, // no BOM in input file name as utf8 string
      &m_impl->m_dirent.d_name[0],
      -1, // null terminated utf8 string
      &current_name[0], ((int)(sizeof(current_name)/sizeof(current_name[0]))) - 1, // output wchar_t string
      0, // null output error status
      (4|8|16), // mask common conversion errors
      0, // error_code_point = null terminator inserted at point of conversion error
      0  // null ouput end-of-string pointer
      );
    // TODO
    //   Test m_dirent.d_name to make sure it passes m_ws/utf8_file_name_filter

    ON_wString wpath = m_directory;
    wpath += '/';
    wpath += current_name;

    // get a utf8 version of the full path to pass to stat
    const ON_String utf8buffer(wpath);
    const char* utf8path = utf8buffer;
    if ( 0 == utf8path )
      continue;

    struct stat buf;
    memset(&buf,0,sizeof(buf));
    int stat_errno = stat( utf8path, &buf);
    if ( 0 != stat_errno )
      continue;

    if ( S_ISDIR(buf.st_mode) )
    {
      current_file_attributes = 2;
    }
    else if ( S_ISREG(buf.st_mode) )
    {
      // Only *.ext filters work at this time for non-windows
      const wchar_t* file_name_filter = m_impl->m_ws_file_name_filter;
      if (   0 != file_name_filter
            && '*' == file_name_filter[0] 
            && '.' == file_name_filter[1]
            && 0 != file_name_filter[2]
            && '*' != file_name_filter[2] )
      {
        // assume this is a *.extension filter
        const wchar_t* current_name_ext = 0;
        on_wsplitpath(current_name,0,0,0,&current_name_ext);
        if (   0 == current_name_ext 
            || 0 != wcscmp(file_name_filter+1,current_name_ext) 
           )
        {
          // current_name does pass match file_name_filter
          continue;
        }
      }
      current_file_attributes = 1;
    }
    else
      continue;

    // save current item information
    memcpy( m_impl->m_current_name, current_name, sizeof(m_impl->m_current_name) );
    m_impl->m_current_file_attributes = current_file_attributes;
    m_impl->m_current_file_size = buf.st_size;
    m_impl->m_current_content_last_modified_time = buf.st_mtime;

    m_item_name = m_impl->m_current_name;
    m_full_path_name = ON_wString::EmptyString;
    return true;
  }

#endif
  const ON__UINT64 saved_count = m_count;
  Reset();
  m_count = saved_count;
  m_state = 3;
  return false;
}

const wchar_t* ON_DirectoryIteratorImpl::CurrentFileNameAsPointer() const
{
#if defined(ON_COMPILER_MSC)
  return ( 0 != m_h && 0 != m_fd.cFileName[0] ) ? m_fd.cFileName : nullptr;
#else
  return ( 0 != m_current_name[0] ) ? m_current_name : nullptr;
#endif
}

const ON_wString ON_FileIterator::CurrentItemName() const
{
  return m_item_name;
}

ON__UINT64 ON_FileIterator::CurrentItemSize() const
{
  ON__UINT64 file_size = 0;

  if (nullptr != m_impl)
  {

#if defined(ON_COMPILER_MSC)
    if (0 != m_impl->CurrentFileNameAsPointer())
    {
      file_size = m_impl->m_fd.nFileSizeHigh;
      file_size *= ((ON__UINT64)0xFFFFFFFF);
      file_size += m_impl->m_fd.nFileSizeLow;
    }
#else
    file_size = m_impl->m_current_file_size;
#endif
  }

  return file_size;
}

bool ON_FileIterator::CurrentItemIsDirectory() const
{
  bool rc = false;
  if (nullptr != m_impl)
  {
    const wchar_t* current_file_name = m_impl->CurrentFileNameAsPointer();
    if (0 != current_file_name && 0 != current_file_name[0])
    {
#if defined(ON_COMPILER_MSC)
      if (0 != (FILE_ATTRIBUTE_DIRECTORY & m_impl->m_fd.dwFileAttributes))
      {
        rc = true;
      }
#else
      if ( 2 == m_impl->m_current_file_attributes)
      {
        rc = true;
      }
#endif
    }
  }
  return rc;
}

bool ON_FileIterator::CurrentItemIsFile() const
{
  bool rc = false;
  if (nullptr != m_impl)
  {
    const wchar_t* current_file_name = m_impl->CurrentFileNameAsPointer();
    if (0 != current_file_name && 0 != current_file_name[0])
    {
#if defined(ON_COMPILER_MSC)
      if (0 == (FILE_ATTRIBUTE_DIRECTORY & m_impl->m_fd.dwFileAttributes))
      {
        rc = true;
      }
#else
      if ( 1 == m_impl->m_current_file_attributes)
      {
        rc = true;
      }
#endif
    }
  }
  return rc;
}
bool ON_FileIterator::CurrentItemIsHidden() const
{
  bool rc = false;
  if (nullptr != m_impl)
  {
    const wchar_t* current_file_name = m_impl->CurrentFileNameAsPointer();
    if (0 != current_file_name && 0 != current_file_name[0])
    {
      if ('.' == current_file_name[0])
      {
        rc = true;
      }
#if defined(ON_COMPILER_MSC)
      else if (0 != (FILE_ATTRIBUTE_HIDDEN & m_impl->m_fd.dwFileAttributes))
      {
        rc = true;
      }
#endif
    }
  }
  return rc;
}


const ON_wString ON_FileIterator::CurrentItemFullPathName() const
{
  if (m_full_path_name.IsEmpty() && m_item_name.IsNotEmpty())
  {
    if (m_directory.IsNotEmpty())
    {
      m_full_path_name = m_directory;
      m_full_path_name += ON_FileSystemPath::DirectorySeparator;
      m_full_path_name += m_item_name;
    }
  }
  return m_full_path_name;
}

ON__UINT64 ON_SecondsSinceJanOne1970UTC()
{
#if defined(ON_COMPILER_MSC)

  __time64_t t = _time64(nullptr);
  return (ON__UINT64)t;  

#elif defined(ON_COMPILER_CLANG) || defined(ON_COMPILER_GNU)

  //__time64_t t = _time64(nullptr);
  time_t t = time(nullptr);
  return (ON__UINT64)t;

#else

  __time64_t t = _time64(nullptr);
  return (ON__UINT64)t;  

#endif
}

const ON_wString SecondsSinceJanOne1970UTCToString(
  ON__UINT64 seconds_since_epoch
  )
{
  int year = 0;
  int month = 0;
  int mday = 0;
  int hour = 0;
  int min = 0;
  int sec = 0;

#if defined(ON_COMPILER_MSC)

  const time_t t = (time_t)seconds_since_epoch;
  const struct tm* ptr = _gmtime64( &t );
  if (nullptr != ptr)
  {
    const struct tm uct = *ptr;
    year = uct.tm_year;
    month = uct.tm_mon;
    mday = uct.tm_mday;
    hour = uct.tm_hour;
    min = uct.tm_min;
    sec = uct.tm_sec;
  }

#elif defined(ON_COMPILER_CLANG) || defined(ON_COMPILER_GNU)

  const time_t t = (time_t)seconds_since_epoch;
  const struct tm* ptr = gmtime( &t );
  if (nullptr != ptr)
  {
    const struct tm uct = *ptr;
    year = uct.tm_year;
    month = uct.tm_mon;
    mday = uct.tm_mday;
    hour = uct.tm_hour;
    min = uct.tm_min;
    sec = uct.tm_sec;
  }

#else

  const time_t t = (time_t)seconds_since_epoch;
  const struct tm* ptr = _gmtime64( &t );
  if (nullptr != ptr)
  {
    const struct tm uct = *ptr;
    year = uct.tm_year;
    month = uct.tm_mon;
    mday = uct.tm_mday;
    hour = uct.tm_hour;
    min = uct.tm_min;
    sec = uct.tm_sec;
  }

#endif

  if (
    year >= 1970
    && month >= 1 && month <= 12
    && mday >= 1 && mday <= 31
    && hour >= 0 && hour <= 24
    && min >= 0 && min <= 60
    && sec >= 0 && sec <= 60
    )
  {
    ON_wString sUTC;
    // yyyy-mm-dd hh:mm:ss
    sUTC.Format(L"%04d-%02d-%02d %02d:%02d:%02d UTC",year,month,mday,hour,min,sec);
    return sUTC;
  }

  return ON_wString::EmptyString;
}

#if defined(ON_COMPILER_MSC)
static ON__UINT64 SecondsSinceJanOne1970( FILETIME ft )
{
  // The FILETIME is in 100-nanosecond intervals since January 1, 1601 UCT.
  //
  // Between midnight January 1, 1601 and midnight January 1, 1970 there 
  // were 134774 days = 11644473600 seconds. Each second has 10^7 intervals
  // that are one hundred nanoseconds long.  So, if N = number of one hundred
  // nanosecond intervals since midnight January 1, 1601, then
  // (N / 10000000) - 11644473600 = number of seconds since midnight
  // January 1, 1970.
  //
  // January 1, 1601 was the start of a Gregorian calendary 400 year cycle
  // and "the internet" sometimes cites that as the reason that date is 
  // the "beginning of time" for Windows' FILETIME values.  This convention
  // would slightly simplify the formulae used to account for leap years, 
  // so it is plausable this might might even be true.

  ON__UINT64 ft_since_jan_1_1601 = ft.dwHighDateTime;
  ft_since_jan_1_1601 *= 0xFFFFFFFF;
  ft_since_jan_1_1601 += ft.dwLowDateTime;

  ON__UINT64 hundrednanoseconds_per_second = 10000000;

  ON__UINT64 seconds_since_jan_1_1601 = ft_since_jan_1_1601 / hundrednanoseconds_per_second;
                                                   
  ON__UINT64 seconds_since_jan_1_1970 = seconds_since_jan_1_1601 - 11644473600;

  return seconds_since_jan_1_1970;
}
#endif

ON__UINT64 ON_FileIterator::CurrentItemLastModifiedTime() const
{
  if ( nullptr == m_impl)
    return 0;
#if defined(ON_COMPILER_MSC)
  return SecondsSinceJanOne1970(m_impl->m_fd.ftLastWriteTime);
#else
  return m_impl->m_current_content_last_modified_time;
#endif
}

ON_FileIterator::~ON_FileIterator()
{
  Reset();
}


ON_ContentHash ON_ContentHash::Create(
  ON_SHA1_Hash sha1_name_hash,
  ON__UINT64 byte_count,
  ON_SHA1_Hash sha1_content_hash,
  ON__UINT64 hash_time,
  ON__UINT64 content_last_modified_time
  )
{
  ON_ContentHash hash;
  
  if ( 0 == hash_time )
    hash_time = ON_SecondsSinceJanOne1970UTC();
  hash.m_byte_count = (byte_count > 0) ? byte_count : 0;
  hash.m_hash_time = hash_time;
  
  hash.m_content_time 
    = (content_last_modified_time <= hash_time) 
    ? content_last_modified_time
    : 0;
  
  hash.m_sha1_name_hash = sha1_name_hash;

  hash.m_sha1_content_hash 
    = (hash.m_byte_count > 0)
    ? sha1_content_hash
    : ON_SHA1_Hash::EmptyContentHash
    ;

  return hash;
}

ON_ContentHash ON_ContentHash::CreateFromBuffer( 
  ON_SHA1_Hash sha1_name_hash,
  const void* buffer,
  size_t byte_count
  )
{
  ON__UINT64 hash_time = ON_SecondsSinceJanOne1970UTC();
  ON__UINT64 hash_byte_count = (nullptr != buffer && byte_count >0) ? ((ON__UINT64)byte_count) : 0;
  ON__UINT64 content_last_modifed_time = 0;
  ON_SHA1_Hash sha1_content_hash = ON_SHA1_Hash::BufferContentHash(buffer,(size_t)hash_byte_count);
  return ON_ContentHash::Create(sha1_name_hash,hash_byte_count,sha1_content_hash,hash_time,content_last_modifed_time);
}

ON_ContentHash ON_ContentHash::CreateFromFile( 
  ON_SHA1_Hash sha1_file_name_hash,
  FILE* fp
  )
{
  ON__UINT64 hash_time = ON_SecondsSinceJanOne1970UTC();
  ON__UINT64 file_byte_count = 0;
  ON__UINT64 file_metadata_last_modified_time = 0;
  ON__UINT64 file_contents_last_modified_time = 0;
  if ( false == ON_FileStream::GetFileInformation(fp,&file_byte_count,&file_metadata_last_modified_time,&file_contents_last_modified_time) )
    return ON_ContentHash::Create(sha1_file_name_hash,0,ON_SHA1_Hash::EmptyContentHash,hash_time,0);

  ON__UINT64 hash_byte_count = 0;
  ON_SHA1_Hash sha1_hash = ON_SHA1_Hash::FileContentHash(fp,hash_byte_count);
  return ON_ContentHash::Create(sha1_file_name_hash,hash_byte_count,sha1_hash,hash_time,file_contents_last_modified_time);
}


ON_ContentHash ON_ContentHash::CreateFromFile( 
  const wchar_t* filename
  )
{
  ON_SHA1_Hash sha1_file_name_hash = (nullptr == filename) ? ON_SHA1_Hash::ZeroDigest : ON_SHA1_Hash::FileSystemPathHash(filename);
  FILE* fp = ON_FileStream::Open(filename, L"rb");
  ON_ContentHash hash = ON_ContentHash::CreateFromFile(sha1_file_name_hash,fp);
  ON_FileStream::Close(fp);
  return hash;
}

ON_ContentHash ON_ContentHash::CreateFromFile( 
  const char* filename
  )
{
  ON_SHA1_Hash sha1_file_name_hash = (nullptr == filename) ? ON_SHA1_Hash::ZeroDigest : ON_SHA1_Hash::FileSystemPathHash(filename);
  FILE* fp = ON_FileStream::Open(filename, "rb");
  ON_ContentHash hash = ON_ContentHash::CreateFromFile(sha1_file_name_hash,fp);
  ON_FileStream::Close(fp);
  return hash;
}


bool ON_ContentHash::EqualContent(
  const ON_ContentHash& a,
  const ON_ContentHash& b
  )
{
  // Do not compare times
  return (a.m_byte_count == b.m_byte_count && a.m_sha1_content_hash == b.m_sha1_content_hash);
}


bool ON_ContentHash::DifferentContent(
  const ON_ContentHash& a,
  const ON_ContentHash& b
  )
{
  // Do not compare times
  return (a.m_byte_count != b.m_byte_count || a.m_sha1_content_hash != b.m_sha1_content_hash);
}

int ON_ContentHash::CompareContent(
  const ON_ContentHash& a,
  const ON_ContentHash& b
  )
{
  if (a.m_byte_count < b.m_byte_count)
    return -1;
  if (a.m_byte_count > b.m_byte_count)
    return 1;
  if (a.m_byte_count < b.m_byte_count)
    return -1;
  if (a.m_byte_count > b.m_byte_count)
    return 1;
  return ON_SHA1_Hash::Compare(a.m_sha1_content_hash, b.m_sha1_content_hash);
}

int ON_ContentHash::Compare(
  const ON_ContentHash& a,
  const ON_ContentHash& b
  )
{
  const int rc = ON_ContentHash::CompareContent(a, b);
  if (0 != rc)
    return rc;

  if (a.m_hash_time < b.m_hash_time)
    return -1;
  if (a.m_hash_time > b.m_hash_time)
    return 1;

  if (a.m_content_time < b.m_content_time)
    return -1;
  if (a.m_content_time > b.m_content_time)
    return 1;

  return ON_SHA1_Hash::Compare(a.m_sha1_name_hash, b.m_sha1_name_hash);
}

bool ON_ContentHash::EqualFileNameSizeAndTime(
  const wchar_t* filename
  ) const
{
  if (IsNotSet())
    return false;
  if ( m_byte_count <= 0 || m_hash_time <= 0 || m_content_time < m_hash_time )
    return false; // content time is not reliable.
  if (nullptr == filename || 0 == filename[0])
    return false;
  const ON_SHA1_Hash sha1_name_hash = ON_SHA1_Hash::StringHash(filename);
  if ( sha1_name_hash != m_sha1_name_hash )
    return false;
  ON__UINT64 file_byte_count = 0;
  ON__UINT64 file_metadata_last_modified_time = 0;
  ON__UINT64 file_contents_last_modified_time = 0;
  if ( false == ON_FileStream::GetFileInformation(filename,&file_byte_count,&file_metadata_last_modified_time,&file_contents_last_modified_time) )
    return false;
  if ( file_contents_last_modified_time <= 0 )
    return false; // content time is not reliable
  return (file_byte_count == m_byte_count && file_contents_last_modified_time == m_content_time);
}

bool ON_ContentHash::IsSet() const
{
  if ( 0 == m_hash_time )
    return false;
  return 
    (0 == m_byte_count)
    ? (ON_SHA1_Hash::EmptyContentHash == m_sha1_content_hash)
    : (ON_SHA1_Hash::EmptyContentHash != m_sha1_content_hash);
}


bool ON_ContentHash::IsNotSet() const
{
  return (false == IsSet());
}

ON__UINT64 ON_ContentHash::ByteCount() const
{
  return m_byte_count;
}


ON__UINT64 ON_ContentHash::HashCalculationTime() const
{
  return m_hash_time;
}


ON__UINT64 ON_ContentHash::ContentLastModifiedTime() const
{
  return m_content_time;
}


ON_SHA1_Hash ON_ContentHash::ContentHash() const
{
  return m_sha1_content_hash;
}


ON_SHA1_Hash ON_ContentHash::NameHash() const
{
  return m_sha1_name_hash;
}
  

bool ON_ContentHash::IsSameBufferContent(
  const void* buffer,
  size_t byte_count
  ) const
{
  return ON_ContentHash::EqualContent(*this, ON_ContentHash::CreateFromBuffer(ON_SHA1_Hash::ZeroDigest,buffer,byte_count));
}


bool ON_ContentHash::IsSameFileContent( 
  FILE* fp
  ) const
{
  return ON_ContentHash::EqualContent(*this, ON_ContentHash::CreateFromFile(ON_SHA1_Hash::ZeroDigest,fp));
}

bool ON_ContentHash::IsSameFileContent( 
  const wchar_t* filename
  ) const
{
  return ON_ContentHash::EqualContent(*this, ON_ContentHash::CreateFromFile(filename));
}

bool ON_ContentHash::IsSameFileContent( 
  const char* filename
  ) const
{
  return ON_ContentHash::EqualContent(*this, ON_ContentHash::CreateFromFile(filename));
}

ON_ContentHash::CompareResult ON_ContentHash::CompareResultFromUnsigned(
  unsigned int compare_result_as_unsigned
  )
{
  switch (compare_result_as_unsigned)
  {
  ON_ENUM_FROM_UNSIGNED_CASE(ON_ContentHash::CompareResult::Unset);
  ON_ENUM_FROM_UNSIGNED_CASE(ON_ContentHash::CompareResult::EqualContent);
  ON_ENUM_FROM_UNSIGNED_CASE(ON_ContentHash::CompareResult::DifferentContent);
  ON_ENUM_FROM_UNSIGNED_CASE(ON_ContentHash::CompareResult::DifferentContentFileIsOlder);
  ON_ENUM_FROM_UNSIGNED_CASE(ON_ContentHash::CompareResult::ContentDifferentFileIsNewer);
  ON_ENUM_FROM_UNSIGNED_CASE(ON_ContentHash::CompareResult::FileDoesNotExist);
  ON_ENUM_FROM_UNSIGNED_CASE(ON_ContentHash::CompareResult::FileSystemFailure);
  }

  ON_ERROR("Invalid compare_result_as_unsigned parameter.");
  return ON_ContentHash::CompareResult::Unset;
}

ON_ContentHash::CompareResult ON_ContentHash::Compare(
  const wchar_t* file_name,
  bool bFastTest
  ) const
{
  if ( false ==  ON_FileSystem::IsFile(file_name) )
    return ON_ContentHash::CompareResult::FileDoesNotExist;

  if (IsNotSet())
    return ON_ContentHash::CompareResult::DifferentContent;

  ON_ContentHash file_content_hash = ON_ContentHash::Unset;
  ON__UINT64 file_metadata_last_modified_time = 0;
  if ( false == ON_FileStream::GetFileInformation(file_name,&file_content_hash.m_byte_count,&file_metadata_last_modified_time,&file_content_hash.m_content_time) )
    return ON_ContentHash::CompareResult::FileSystemFailure;
  if (0 == file_content_hash.m_byte_count && 0 == file_metadata_last_modified_time && 0 == file_content_hash.m_content_time)
  {
    return ON_ContentHash::CompareResult::FileSystemFailure;
  }

  const ON__UINT64 current_time = ON_SecondsSinceJanOne1970UTC();
  bool bValidContentHashTime = (m_content_time > 0 && m_hash_time >= m_content_time && current_time >= m_hash_time);
  bool bValidFileTime = (file_content_hash.m_content_time > 0 && current_time >= file_content_hash.m_content_time);
  if (file_content_hash.m_byte_count != m_byte_count)
  {
    if (bValidContentHashTime &&  bValidFileTime)
    {
      // assume time values are accurate.
      if ( file_content_hash.m_content_time < m_content_time )
        return ON_ContentHash::CompareResult::DifferentContentFileIsOlder;
      if ( file_content_hash.m_content_time > m_content_time )
        return ON_ContentHash::CompareResult::ContentDifferentFileIsNewer;
    }
    return ON_ContentHash::CompareResult::DifferentContent;
  }

  file_content_hash.m_sha1_name_hash = ON_SHA1_Hash::StringHash(file_name);
  file_content_hash.m_sha1_content_hash = m_sha1_content_hash;

  if (bValidContentHashTime && bValidFileTime
    && m_content_time == file_content_hash.m_content_time
    && m_byte_count == file_content_hash.m_byte_count
    && m_sha1_name_hash == file_content_hash.m_sha1_name_hash
    )
  {
    if (bFastTest)
      return ON_ContentHash::CompareResult::EqualContent;
  }

  // Have to calculate SHA1 content hash
  file_content_hash = ON_ContentHash::CreateFromFile(file_name);
  return ON_ContentHash::Compare(file_content_hash);
}

ON_ContentHash::CompareResult ON_ContentHash::Compare(
  ON_ContentHash file_content_hash
  ) const
{
  if (file_content_hash.IsNotSet())
  {
    return ( m_sha1_name_hash == ON_SHA1_Hash::EmptyContentHash)
      ? ON_ContentHash::CompareResult::FileDoesNotExist
      : ON_ContentHash::CompareResult::FileSystemFailure;
  }
  if ( IsNotSet() )
    return ON_ContentHash::CompareResult::DifferentContent;
  if ( m_byte_count == file_content_hash.m_byte_count && m_sha1_content_hash == file_content_hash.m_sha1_content_hash )
    return ON_ContentHash::CompareResult::EqualContent;
  const ON__UINT64 current_time = ON_SecondsSinceJanOne1970UTC();
  bool bValidTimes 
    =  m_content_time > 0 
    && m_hash_time >= m_content_time 
    && current_time >= m_hash_time
    && file_content_hash.m_content_time > 0 
    && file_content_hash.m_hash_time >= file_content_hash.m_content_time 
    && current_time >= file_content_hash.m_hash_time
    ;
  if (bValidTimes)
  {
    if ( file_content_hash.m_content_time < m_content_time )
      return ON_ContentHash::CompareResult::DifferentContentFileIsOlder;
    if ( file_content_hash.m_content_time > m_content_time )
      return ON_ContentHash::CompareResult::ContentDifferentFileIsNewer;
  }
  return ON_ContentHash::CompareResult::DifferentContent;
}

bool ON_ContentHash::Read(
  class ON_BinaryArchive& archive
  )
{
  *this = ON_ContentHash::Unset;
  bool rc = false;
  int major_version = 0;
  int minor_version = 0;
  if (!archive.BeginRead3dmChunk(TCODE_ANONYMOUS_CHUNK,&major_version,&minor_version))
    return rc;
  for (;;)
  {
    if ( 1 != major_version )
      break;
    if (!archive.ReadBigInt(&m_byte_count))
      break;
    if (!archive.ReadBigInt(&m_hash_time))
      break;
    if (!archive.ReadBigInt(&m_content_time))
      break;
    if (!m_sha1_name_hash.Read(archive))
      break;
    if (!m_sha1_content_hash.Read(archive))
      break;
    rc = true;
    break;
  }
  if (!archive.EndRead3dmChunk())
    rc = false;
  return rc;
}


bool ON_ContentHash::Write(
  class ON_BinaryArchive& archive
  ) const
{
  if (!archive.BeginWrite3dmChunk(TCODE_ANONYMOUS_CHUNK,1,0))
    return false;
  bool rc = false;
  for (;;)
  {
    if (!archive.WriteBigInt(m_byte_count))
      break;
    if (!archive.WriteBigInt(m_hash_time))
      break;
    if (!archive.WriteBigInt(m_content_time))
      break;
    if (!m_sha1_name_hash.Write(archive))
      break;
    if (!m_sha1_content_hash.Write(archive))
      break;
    rc = true;
    break;
  }
  if (!archive.EndWrite3dmChunk())
    rc = false;
  return rc;
}

void ON_ContentHash::Dump(
  class ON_TextLog& text_log
  ) const
{
  if (IsSet())
  {
    text_log.Print(L"ON_ContentHash:\n");
    text_log.PushIndent();

    text_log.Print(L"Name hash: ");
    m_sha1_name_hash.Dump(text_log);
    text_log.Print(L"Content byte count = %llu\n",m_byte_count);
    text_log.Print(L"Content hash: ");
    m_sha1_content_hash.Dump(text_log);

    const ON_wString content_time
      = ( m_content_time <= 0 )
      ? L"unknown"
      : SecondsSinceJanOne1970UTCToString(m_content_time);
    text_log.Print(L"Content last modified time = %ls\n",static_cast<const wchar_t*>(content_time));

    const ON_wString hash_time
      = ( m_hash_time <= 0 )
      ? L"unknown"
      : SecondsSinceJanOne1970UTCToString(m_hash_time);
    text_log.Print(L"Content hash calculated time = %ls\n",static_cast<const wchar_t*>(content_time));

    text_log.PopIndent();
  }
  else
  {
    text_log.Print(L"ON_ContentHash::Unset\n");
  }
}

int ON_FileReference::Compare(
  const ON_FileReference& a,
  const ON_FileReference& b
  )
{
  int rc;
  for (;;)
  {
    // must compare every byte of every field.
    // If you don't like that, add another clearly named compare function.
    rc = ON_wString::ComparePath(a.m_full_path,b.m_full_path);
    if (0 != rc)
      break;
    rc = ON_wString::CompareOrdinal(a.m_full_path,b.m_full_path,false);
    if (0 != rc)
      break;
    rc = ON_wString::ComparePath(a.m_relative_path,b.m_relative_path);
    if (0 != rc)
      break;
    rc = ON_wString::CompareOrdinal(a.m_relative_path,b.m_relative_path,false);
    if (0 != rc)
      break;
    rc = ON_ContentHash::CompareContent(a.m_content_hash, b.m_content_hash);
    if (0 != rc)
      break;

    break;
  }
  return rc;
}


ON_FileReference::Status ON_FileReference::StatusFromUnsigned(
  unsigned int full_path_status_as_unsigned
  )
{
  switch (full_path_status_as_unsigned)
  {
  ON_ENUM_FROM_UNSIGNED_CASE(ON_FileReference::Status::Unknown);
  ON_ENUM_FROM_UNSIGNED_CASE(ON_FileReference::Status::FullPathValid);
  ON_ENUM_FROM_UNSIGNED_CASE(ON_FileReference::Status::FileNotFound);
  }
  ON_ERROR("Invalid parameter.");
  return ON_FileReference::Status::Unknown;
}


ON_FileReference::ON_FileReference(
  const wchar_t* full_path,
  const wchar_t* relative_path,
  ON_ContentHash content_hash,
  ON_FileReference::Status full_path_status
  )
  : m_full_path(full_path)
  , m_relative_path(relative_path)
  , m_content_hash(content_hash)
  , m_full_path_status(full_path_status)
{}

ON_FileReference ON_FileReference::CreateFromFullPath(
  const wchar_t* full_path,
  bool bSetContentHash,
  bool bSetFullPathStatus
  )
{
  ON_wString local_full_path(full_path);
  local_full_path.TrimLeftAndRight();
  if (local_full_path.IsEmpty())
    return ON_FileReference::Unset;
  full_path = local_full_path;
  ON_FileReference::Status full_path_status = ON_FileReference::Unset.m_full_path_status;
  ON_ContentHash content_hash = ON_FileReference::Unset.m_content_hash;
  const bool bFileExists =
    ( bSetFullPathStatus || bSetContentHash )
    ? ON_FileSystem::IsFile(full_path)
    : false;
  if ( bSetFullPathStatus && bFileExists )
    full_path_status = ON_FileReference::Status::FullPathValid;
  if ( bSetContentHash && bFileExists )
    content_hash = ON_ContentHash::CreateFromFile(full_path);

  const wchar_t* relative_path = nullptr;
  const wchar_t* v = nullptr;
  const wchar_t* d = nullptr;
  const wchar_t* f = nullptr;
  const wchar_t* e = nullptr;
  on_wsplitpath(full_path,&v,&d,&f,&e);
  if (nullptr != d && nullptr != f && d < f && '.' == d[0])
  {
    relative_path = full_path;
    full_path = nullptr;
  }

  return ON_FileReference(
    full_path,
    relative_path,
    content_hash,
    full_path_status
    );
}


ON_FileReference::FindFilePreference ON_FileReference::FindFile(
  const wchar_t* base_path,
  bool bBasePathIncludesFileName,
  ON_wString& found_file_full_path
  ) const
{
  const ON_FileReference::FindFilePreference* file_preference = nullptr;
  const unsigned int file_preference_count = 0;
  return Internal_FindFile(
    base_path,
    bBasePathIncludesFileName,
    file_preference,
    file_preference_count,
    found_file_full_path,
    nullptr
    );
}

ON_FileReference::FindFilePreference ON_FileReference::FindFile(
  const wchar_t* base_path,
  bool bBasePathIncludesFileName,
  ON_FileReference::FindFilePreference first_choice,
  ON_FileReference::FindFilePreference second_choice,
  ON_FileReference::FindFilePreference third_choice,
  ON_FileReference::FindFilePreference forth_choice,
  ON_FileReference::FindFilePreference fifth_choice,
  ON_wString& found_file_full_path
  ) const
{
  const ON_FileReference::FindFilePreference file_preference[] = { first_choice, second_choice, third_choice, forth_choice, fifth_choice };
  const unsigned int file_preference_count = (unsigned int)(sizeof(file_preference)/sizeof(file_preference[0]));
  return Internal_FindFile(
    base_path,
    bBasePathIncludesFileName,
    file_preference,
    file_preference_count,
    found_file_full_path,
    nullptr
    );
}

ON_FileReference::FindFilePreference ON_FileReference::FindFileAndUpdateReference(
  const wchar_t* base_path,
  bool bBasePathIncludesFileName,
  bool bUpdateContentHash
  )
{
  ON_wString found_file_full_path;
  return FindFileAndUpdateReference(base_path,bBasePathIncludesFileName,bUpdateContentHash,found_file_full_path);
}

ON_FileReference::FindFilePreference ON_FileReference::FindFileAndUpdateReference(
  const wchar_t* base_path,
  bool bBasePathIncludesFileName,
  bool bUpdateContentHash,
  ON_wString& found_file_full_path
  )
{
  return FindFileAndUpdateReference(
    base_path,
    bBasePathIncludesFileName,
    ON_FileReference::FindFilePreference::None,
    ON_FileReference::FindFilePreference::None,
    ON_FileReference::FindFilePreference::None,
    ON_FileReference::FindFilePreference::None,
    ON_FileReference::FindFilePreference::None,
    bUpdateContentHash,
    found_file_full_path
    );
}

ON_FileReference::FindFilePreference ON_FileReference::FindFileAndUpdateReference(
  const wchar_t* base_path,
  bool bBasePathIncludesFileName,
  ON_FileReference::FindFilePreference first_choice,
  ON_FileReference::FindFilePreference second_choice,
  ON_FileReference::FindFilePreference third_choice,
  ON_FileReference::FindFilePreference forth_choice,
  ON_FileReference::FindFilePreference fifth_choice,
  bool bUpdateContentHash,
  ON_wString& found_file_full_path
  )
{
  const ON_FileReference::FindFilePreference file_preference[] = { first_choice, second_choice, third_choice, forth_choice, fifth_choice };
  const unsigned int file_preference_count = (unsigned int)(sizeof(file_preference)/sizeof(file_preference[0]));
  ON_ContentHash found_file_content_hash = ON_ContentHash::Unset;
  ON_FileReference::FindFilePreference rc = Internal_FindFile(
    base_path,
    bBasePathIncludesFileName,
    file_preference,
    file_preference_count,
    found_file_full_path,
    &found_file_content_hash
    );
  if (rc != ON_FileReference::FindFilePreference::None && found_file_full_path.IsNotEmpty())
  {
    m_full_path = found_file_full_path;
    m_relative_path = ON_wString::EmptyString;
    m_full_path_hash = ON_SHA1_Hash::EmptyContentHash;
    m_embedded_file_id = ON_nil_uuid;
    if ( bUpdateContentHash && found_file_content_hash.IsNotSet() )
      found_file_content_hash = ON_ContentHash::CreateFromFile(m_full_path);
    if ( found_file_content_hash.IsSet() )
      m_content_hash = found_file_content_hash;
  }
  return rc;
}

static ON_FileReference::FindFilePreference Internal_FindFileResult(
  const ON_wString& file_name_result,
  const ON_ContentHash& content_hash_result,
  ON_FileReference::FindFilePreference rc,
  ON_wString& found_file_full_path,
  ON_ContentHash* found_file_content_hash
  )
{
  found_file_full_path = file_name_result;
  if (nullptr != found_file_content_hash)
    *found_file_content_hash = content_hash_result;
  return rc;
}

ON_FileReference::FindFilePreference ON_FileReference::Internal_FindFile(
  const wchar_t* base_path,
  bool bBasePathIncludesFileName,
  const ON_FileReference::FindFilePreference* file_preference,
  unsigned int file_preference_count,
  ON_wString& found_file_full_path,
  ON_ContentHash* found_file_content_hash
  ) const
{
  for (;;)
  {
    if (m_full_path.IsEmpty())
      break;

    const wchar_t* filename = nullptr;
    on_wsplitpath(static_cast<const wchar_t*>(m_full_path), nullptr, nullptr, &filename, nullptr);
    if (nullptr == filename || 0 == filename[0])
      break;

    // Clean up base_path
    ON_wString local_base_path(base_path);
    base_path = nullptr;
    local_base_path.TrimLeftAndRight();

    if ( local_base_path.IsNotEmpty() )
    {
      // When the caller is confused and local_base_path identifies an existing file system element,
      // the set the bBasePathIncludesFileName parameter correctly.
      if (bBasePathIncludesFileName)
      {
        if (ON_FileSystem::IsDirectory(base_path))
          bBasePathIncludesFileName = false;
      }
      else
      {
        if (ON_FileSystem::IsFile(base_path))
          bBasePathIncludesFileName = true;
      }
    }

    if (local_base_path.IsNotEmpty() && bBasePathIncludesFileName)
    {
      bBasePathIncludesFileName = false;
      const wchar_t* start = static_cast<const wchar_t*>(local_base_path);
      const wchar_t* end_mark = nullptr;
      on_wsplitpath(start, nullptr, nullptr, &end_mark, nullptr);
      if (nullptr != start && nullptr != end_mark && start < end_mark)
      {
        local_base_path.SetLength(end_mark - start);
        if (false == ON_FileSystemPath::IsDirectorySeparator(local_base_path[local_base_path.Length() - 1], true))
          local_base_path += ON_FileSystemPath::DirectorySeparator;
        base_path = local_base_path;
      }
    }
    

    // Clean up file preferences and append defaults
    ON_FileReference::FindFilePreference default_pref[] =
    {
      ON_FileReference::FindFilePreference::RelativePath,
      ON_FileReference::FindFilePreference::FullPath,
      ON_FileReference::FindFilePreference::ContentMatch,
      ON_FileReference::FindFilePreference::BasePath,
      ON_FileReference::FindFilePreference::MostRecent
    };
    ON_FileReference::FindFilePreference pref[10 + (sizeof(default_pref) / sizeof(default_pref[0]))];
    unsigned int pref_capacity = (unsigned int)(sizeof(pref) / sizeof(pref[0]));
    unsigned int pref_count = 0;
    for (unsigned int pass = 0; pass < 2; pass++)
    {
      const ON_FileReference::FindFilePreference* pref_source = nullptr;
      unsigned int pref_source_count = 0;
      if (0 == pass)
      {
        pref_source = file_preference;
        pref_source_count = file_preference_count;
      }
      else if (1 == pass)
      {
        pref_source = default_pref;
        pref_source_count = (unsigned int)(sizeof(default_pref) / sizeof(default_pref[0]));
      }
      if (nullptr != pref_source)
        continue;
      for (unsigned int i = 0; i < pref_source_count && pref_count < pref_capacity; i++)
      {
        if (ON_FileReference::FindFilePreference::None == pref_source[i])
          continue;
        unsigned int j;
        for (j = 0; j < i; j++)
        {
          if (pref[j] == pref_source[i])
            break;
        }
        if (j < i)
          continue; // don't add duplicate
        if (pref_count < i)
          pref[pref_count] = pref[i];
        pref_count++;
      }
    }


    ON_wString candidate_file_name[3];  // full path, base path + relative path, base path + file_name
    ON_FileReference::FindFilePreference candidate_file_pref[3] = { ON_FileReference::FindFilePreference::None, ON_FileReference::FindFilePreference::None, ON_FileReference::FindFilePreference::None };
    unsigned int candidate_count = 0;
    for (unsigned int pass = 0; pass < 3; pass++)
    {
      ON_FileReference::FindFilePreference ffp = ON_FileReference::FindFilePreference::None;
      ON_wString name;
      switch (pass)
      {
      case 0:
        name = m_full_path;
        ffp = ON_FileReference::FindFilePreference::FullPath;
        break;
      case 1:
        if (nullptr != base_path && m_relative_path.IsNotEmpty())
        {
          name = ON_FileSystemPath::FullPathFromRelativePath(base_path,false,static_cast<const wchar_t*>(m_relative_path));
          ffp = ON_FileReference::FindFilePreference::RelativePath;
        }
        break;
      case 2:
        if (nullptr != base_path)
        {
          name = ON_FileSystemPath::FullPathFromRelativePath(base_path,false,filename);
          ffp = ON_FileReference::FindFilePreference::BasePath;
        }
        break;
      }
      if (name.IsEmpty() || ON_FileReference::FindFilePreference::None == ffp)
        continue;
      if ( false == ON_FileSystem::IsFile(name) )
        continue;
      if (ffp == pref[0])
      {
        // got lucky
        return Internal_FindFileResult(
          name,
          ON_ContentHash::Unset,
          ffp,
          found_file_full_path,
          found_file_content_hash);
      }
      candidate_file_name[candidate_count] = name;
      candidate_file_pref[candidate_count] = ffp;
      candidate_count++;
    }


    if (0 == candidate_count)
      break;

    if ( 1 == candidate_count )
    {
      return Internal_FindFileResult(
        candidate_file_name[0],
        ON_ContentHash::Unset,
        candidate_file_pref[0],
        found_file_full_path,
        found_file_content_hash);
    }


    ON_ContentHash candidate_file_content[3] = { ON_ContentHash::Unset, ON_ContentHash::Unset, ON_ContentHash::Unset };
    ON__UINT64 candidate_file_time[3] = { 0 };

    for (unsigned int i = 0; i < pref_count; i++)
    {
      switch (pref[i])
      {
      case ON_FileReference::FindFilePreference::None:
        break;

      case ON_FileReference::FindFilePreference::FullPath:
      case ON_FileReference::FindFilePreference::RelativePath:
      case ON_FileReference::FindFilePreference::BasePath:
        for (unsigned int j = 0; j < candidate_count; j++)
        {
          if (pref[i] == candidate_file_pref[j])
          {
            return Internal_FindFileResult(
              candidate_file_name[j],
              candidate_file_content[j],
              candidate_file_pref[j],
              found_file_full_path,
              found_file_content_hash);
          }
        }
        break;

      case ON_FileReference::FindFilePreference::ContentMatch:
        for (unsigned int j = 0; j < candidate_count; j++)
        {
          if (candidate_file_content[j].IsNotSet())
          {
            for (unsigned int k = 0; k < j; k++)
            {
              if (ON_wString::EqualPath(candidate_file_name[j], candidate_file_name[k]))
              {
                candidate_file_content[j] = candidate_file_content[k];
                break;
              }
            }
            if (candidate_file_content[j].IsNotSet())
            {
              // Use EqualFileNameSizeAndTime() to avoid expensive content calculation.
              if (ON_FileReference::FindFilePreference::FullPath == candidate_file_pref[j]
                && m_content_hash.EqualFileNameSizeAndTime(candidate_file_name[j]))
                candidate_file_content[j] = m_content_hash;
              else
                candidate_file_content[j] = ON_ContentHash::CreateFromFile(candidate_file_name[j]);
            }
          }
          if (candidate_file_content[j].IsSet())
          {
            if (ON_ContentHash::EqualContent(m_content_hash, candidate_file_content[j]))
              return Internal_FindFileResult(
                candidate_file_name[j],
                candidate_file_content[j],
                ON_FileReference::FindFilePreference::ContentMatch,
                found_file_full_path,
                found_file_content_hash);
            candidate_file_time[j] = candidate_file_content[j].ContentLastModifiedTime();
          }
        }
        break;

      case ON_FileReference::FindFilePreference::MostRecent:
        {
          unsigned int most_recent_dex = candidate_count;
          ON__UINT64 most_recent_time = 0;
          for (unsigned int j = 0; j < candidate_count; j++)
          {
            if (candidate_file_time[j] <= 0)
            {
              ON__UINT64 t = 0;
              for (unsigned int k = 0; k < j; k++)
              {
                if (ON_wString::EqualPath(candidate_file_name[j], candidate_file_name[k]))
                {
                  t = candidate_file_time[k];
                  break;
                }
              }
              if (t <= 0)
                ON_FileStream::GetFileInformation(candidate_file_name[j], nullptr, nullptr, &t);
              candidate_file_time[j] = t;
            }
            if (candidate_file_time[j] > most_recent_time)
            {
              most_recent_dex = j;
              most_recent_time = candidate_file_time[j];
            }
          }
          if (most_recent_time > 0 && most_recent_dex < candidate_count)
            return Internal_FindFileResult(
            candidate_file_name[most_recent_dex],
            candidate_file_content[most_recent_dex],
            ON_FileReference::FindFilePreference::MostRecent,
            found_file_full_path,
            found_file_content_hash);
        }
        break;

      default:
        break;
      }
    }

    return Internal_FindFileResult(
      candidate_file_name[0],
      candidate_file_content[0],
      candidate_file_pref[0],
      found_file_full_path,
      found_file_content_hash);
    break;
  }

  // file not found
  return Internal_FindFileResult(
    ON_wString::EmptyString,
    ON_ContentHash::Unset,
    ON_FileReference::FindFilePreference::None,
    found_file_full_path,
    found_file_content_hash);
}

bool ON_FileReference::IsSet() const
{
  return m_full_path.IsNotEmpty();
}

bool ON_FileReference::IsNotSet() const
{
  return m_full_path.IsEmpty();
}


bool ON_FileReference::Write(
  bool bUseArchiveBasePath,
  ON_BinaryArchive& archive
  ) const
{
  const wchar_t* base_path
    = bUseArchiveBasePath
    ? archive.ArchiveDirectoryNameAsPointer()
    : nullptr;
  bool bBasePathIncludesFileName = false;
  return Write( base_path, bBasePathIncludesFileName, archive );
}

bool ON_FileReference::Write(
  const wchar_t* base_path,
  bool bBasePathIncludesFileName,
  ON_BinaryArchive& archive
  ) const
{
  const int major_version = 1;

  // the embedded file id was added minor version 1
  const int minor_version = archive.Archive3dmVersion() >= 60 ? 1 : 0;

  if (!archive.BeginWrite3dmChunk(TCODE_ANONYMOUS_CHUNK,major_version,minor_version))
    return false;

  bool rc = false;
  for (;;)
  {
    const bool bBasePathIsEmpty = (nullptr == base_path || 0 == base_path[0]);

    const ON_wString full_path
      = m_full_path.IsEmpty() && m_relative_path.IsNotEmpty() && false == bBasePathIsEmpty
      ? ON_FileSystemPath::CombinePaths(base_path, bBasePathIncludesFileName, m_relative_path, true, false)
      : m_full_path;
    if (!archive.WriteString(full_path))
      break;

    const ON_wString relative_path 
      = (bBasePathIsEmpty || m_full_path.IsEmpty() )
      ? m_relative_path
      : ON_FileSystemPath::RelativePath(m_full_path,true,base_path,bBasePathIncludesFileName);
    if (!archive.WriteString(relative_path))
      break;

    if (!m_content_hash.Write(archive))
      break;
    unsigned int i = static_cast<unsigned int>(m_full_path_status);
    if (!archive.WriteInt(i))
      break;

    // embedded file id added at chunk version 1.1
    ON_UUID embedded_file_id = m_embedded_file_id;

    if ( IsSet() && archive.Active3dmTable() >  ON_3dmArchiveTableType::bitmap_table )
    {
      const ON_ComponentManifestItem& embedded_file_item = archive.Manifest().ItemFromNameHash(
        ON_ModelComponent::Type::Image,
        ON_NameHash::CreateFilePathHash(*this)
        );
      if (embedded_file_item.IsValid())
      {
        // A file with identical full path is embedded in this archive.
        // The embedded file can be used if the referenced file cannot
        // be found when this archive is read.
        embedded_file_id = embedded_file_item.Id();
      }
    }

    if (!archive.WriteUuid(embedded_file_id))
      break;

    rc = true;
    break;
  }

  if (!archive.EndWrite3dmChunk())
    rc = false;
  return rc;
}

bool ON_FileReference::Read(
  ON_BinaryArchive& archive
  )
{
  *this = ON_FileReference::Unset;
  int major_version = 0;
  int minor_version = 0;
  if (!archive.BeginRead3dmChunk(TCODE_ANONYMOUS_CHUNK,&major_version,&minor_version))
    return false;
  bool rc = false;
  for (;;)
  {
    if ( 1 != major_version)
      break;
    if (!archive.ReadString(m_full_path))
      break;
    if (!archive.ReadString(m_relative_path))
      break;
    if (!m_content_hash.Read(archive))
      break;
    
    unsigned int full_path_status_as_unsigned = 0;
    if (!archive.ReadInt(&full_path_status_as_unsigned))
      break;
    //m_full_path_status = ON_FileReference::StatusFromUnsigned(full_path_status_as_unsigned);
    // The full path status must be validated after each read.
    m_full_path_status = ON_FileReference::Status::Unknown; 

    if (minor_version >= 1)
    {
      if (!archive.ReadUuid(m_embedded_file_id))
        break;
    }
    rc = true;
    break;
  }

  if (!archive.EndRead3dmChunk())
    rc = false;
  return rc;

}

void ON_FileReference::Dump(
  class ON_TextLog& text_log
  ) const
{
  text_log.Print("Full path: \"%ls\"\n", static_cast<const wchar_t*>(m_full_path));
  text_log.PushIndent();
  text_log.Print("Relative path: \"%ls\"\n", static_cast<const wchar_t*>(m_relative_path));
  m_content_hash.Dump(text_log);
  text_log.PopIndent();
}

unsigned int ON_FileReference::SizeOf() const
{
  return m_full_path.SizeOf() + m_relative_path.SizeOf() + (unsigned int)(sizeof(*this) - sizeof(m_full_path) - sizeof(m_relative_path));
}

const ON_wString& ON_FileReference::FullPath() const
{
  return m_full_path;
}

const wchar_t* ON_FileReference::FullPathAsPointer() const
{
  return static_cast<const wchar_t*>(m_full_path);
}

void ON_FileReference::SetFullPath(
  const wchar_t* full_path,
  bool bSetContentHash
  )
{
  ON_wString local_full_path(full_path);
  local_full_path.TrimLeftAndRight();
  if (local_full_path.IsEmpty())
    *this = ON_FileReference::Unset;
  else
  {
    const ON_wString clean_full_path = ON_FileSystemPath::CleanPath(local_full_path);
    const bool bFullPathChanged = (0 != ON_wString::CompareOrdinal(clean_full_path, m_full_path, false));
    m_full_path = clean_full_path;
    m_full_path_hash = ON_SHA1_Hash::EmptyContentHash;
    m_embedded_file_id = ON_nil_uuid;
    m_relative_path = ON_wString::EmptyString;
    if (bSetContentHash)
    {
      if (m_content_hash.IsNotSet() || false == m_content_hash.EqualFileNameSizeAndTime(m_full_path))
        m_content_hash = ON_ContentHash::CreateFromFile(m_full_path);
      m_full_path_status
        =  (m_content_hash.IsSet())
        ? ON_FileReference::Status::FullPathValid
        : ON_FileReference::Status::FileNotFound;
    }
    else if (bFullPathChanged)
    {
      m_content_hash = ON_ContentHash::Unset;
      m_full_path_status 
        = ON_FileSystem::IsFile(m_full_path)
        ? ON_FileReference::Status::FullPathValid
        : ON_FileReference::Status::FileNotFound;
    }
  }
}

void ON_FileReference::SetFullPath(
  const char* full_path,
  bool bSetContentHash
  )
{
  const ON_wString local_full_path(full_path);
  SetFullPath(static_cast<const wchar_t*>(local_full_path),bSetContentHash);
}



void ON_FileReference::ClearFullPath()
{
  m_full_path = ON_wString::EmptyString;
  m_full_path_hash = ON_SHA1_Hash::EmptyContentHash;
  m_embedded_file_id = ON_nil_uuid;
  m_full_path_status = ON_FileReference::Status::Unknown;
}

const ON_wString& ON_FileReference::RelativePath() const
{
  return m_relative_path;
}

const wchar_t* ON_FileReference::RelativePathAsPointer() const
{
  return static_cast<const wchar_t*>(m_relative_path);
}

void ON_FileReference::SetRelativePath(
  const wchar_t* relative_path
  )
{
  m_relative_path = relative_path;
  m_relative_path.TrimLeftAndRight();
}

void ON_FileReference::SetRelativePath(
  const char* relative_path
  )
{
  m_relative_path = relative_path;
  m_relative_path.TrimLeftAndRight();
}

void ON_FileReference::SetRelativePathFromBasePath(
  const wchar_t* base_path,
  bool bBasePathContainsFileName
  )
{
  const ON_wString relative_path = ON_FileSystemPath::RelativePath(
    m_full_path,
    true,
    base_path,
    bBasePathContainsFileName
    );
}

void ON_FileReference::SetRelativePathFromBasePath(
  const char* base_path,
  bool bBasePathContainsFileName
  )
{
  const ON_wString local_base_path(base_path);
  SetRelativePathFromBasePath(static_cast<const wchar_t*>(local_base_path),bBasePathContainsFileName);
}

void ON_FileReference::ClearRelativePath()
{
  m_relative_path = ON_wString::EmptyString;
}

const ON_ContentHash& ON_FileReference::ContentHash() const
{
  return m_content_hash;
}

void ON_FileReference::SetContentHash(
  ON_ContentHash content_hash
  )
{
  m_content_hash = content_hash;
}

void ON_FileReference::ClearContentHash()
{
  m_content_hash = ON_ContentHash::Unset;
}

bool ON_FileReference::UpdateContentHash()
{
  if (m_full_path.IsEmpty())
  {
    m_content_hash = ON_FileReference::Unset.ContentHash();
    return true;
  }
  m_content_hash = ON_ContentHash::CreateFromFile(m_full_path);
  m_recent_content_hash = m_content_hash;
  return m_content_hash.IsSet();
}

const ON_ContentHash& ON_FileReference::RecentContentHash(
  ON__UINT64 recent_time
) const
{
  const ON__UINT64 current_time = ON_SecondsSinceJanOne1970UTC();
  if (0 == recent_time || recent_time > current_time)
    recent_time = current_time;
  if (m_recent_content_hash.IsNotSet() || m_recent_content_hash.HashCalculationTime() < recent_time)
  {
    if (m_content_hash.IsSet() && m_content_hash.HashCalculationTime() >= recent_time)
      m_recent_content_hash = m_content_hash;
    else
      m_recent_content_hash = ON_ContentHash::CreateFromFile(m_full_path);
  }
  return m_recent_content_hash;
}

const ON_SHA1_Hash& ON_FileReference::FullPathHash() const
{
  if (m_full_path.IsNotEmpty() && m_full_path_hash == ON_SHA1_Hash::EmptyContentHash)
  {
    m_full_path_hash = ON_SHA1_Hash::FileSystemPathHash(m_full_path);
  }
  return m_full_path_hash;
}

ON_FileReference::Status ON_FileReference::FullPathStatus() const
{
  return m_full_path_status;
}

void ON_FileReference::SetFullPathStatus(
  ON_FileReference::Status full_path_status
  )
{
  m_full_path_status = full_path_status;
}

ON_UUID ON_FileReference::EmbeddedFileId() const
{
  return m_embedded_file_id;
}

void ON_FileReference::SetEmbeddedFileId(
  ON_UUID embedded_file_id
  )
{
  m_embedded_file_id = embedded_file_id;
}

// deprecated
bool ON_FileSystemPath::PathExists(
  const char* path
)
{
  return ON_FileSystem::PathExists(path);
}

// deprecated
bool ON_FileSystemPath::PathExists(
  const wchar_t* path
)
{
  return ON_FileSystem::PathExists(path);
}

// deprecated
bool ON_FileSystemPath::IsFile(
  const char* path
)
{
  return ON_FileSystem::IsFile(path);
}

// deprecated
bool ON_FileSystemPath::IsFile(
  const wchar_t* path
)
{
  return ON_FileSystem::IsFile(path);
}

// deprecated
bool ON_FileSystemPath::IsDirectory(
  const char* path
)
{
  return ON_FileSystem::IsDirectory(path);
}

// deprecated
bool ON_FileSystemPath::IsDirectory(
  const wchar_t* path
)
{
  return ON_FileSystem::IsDirectory(path);
}
