// // Copyright (c) 1993-2022 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 . // //////////////////////////////////////////////////////////////// #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 #pragma ON_PRAGMA_WARNING_AFTER_DIRTY_INCLUDE #pragma ON_PRAGMA_WARNING_BEFORE_DIRTY_INCLUDE #include #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(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(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(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(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) char sz[PATH_MAX]; getcwd(sz, PATH_MAX); return sz; #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(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(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(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(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 character 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(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(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(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(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(utf8_file_path)); #else const ON_String utf8_file_path(file_path); rc = std::remove(static_cast(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(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(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(local_full); const wchar_t* base_tail = static_cast(local_base); if (false == IsDirSep(*full_tail) || false == IsDirSep(*base_tail)) { // A double directory separator after the initial CleanAndRemoveFileName() // calls indicates invalid file path information. 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 information. 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(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_COCOA_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_COCOA_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(local_directory_name), static_cast(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(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(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 ¤t_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,¤t_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); } #include using ContentHashMap = std::unordered_map; std::weak_ptr g_pContentHashCache; class ON_ContentHash::Cache::Private { public: std::shared_ptr p; }; ON_ContentHash::Cache::Cache() : m_private(new Private) { m_private->p = g_pContentHashCache.lock(); if (!m_private->p) { m_private->p.reset(new ContentHashMap); g_pContentHashCache = m_private->p; } } ON_ContentHash::Cache::~Cache() { delete m_private; } void ON_ContentHash::Cache::Add(const wchar_t* path, const ON_ContentHash& hash) { auto map = g_pContentHashCache.lock(); if (map) { map->insert(std::make_pair(path, hash)); } } const ON_ContentHash* ON_ContentHash::Cache::FromFile(const wchar_t* path) { auto map = g_pContentHashCache.lock(); if (map) { auto it = map->find(path); if (it != map->end()) { return &it->second; } } return nullptr; } const ON_ContentHash* ON_ContentHash::Cache::FromFile(const char* p) { ON_wString s(p); return FromFile((const wchar_t*)s); } void ON_ContentHash::Cache::Add(const char* p, const ON_ContentHash& h) { ON_wString s(p); Add(s, h); } ON_ContentHash ON_ContentHash::CreateFromFile( const wchar_t* filename ) { if (auto pHash = Cache::FromFile(filename)) { return *pHash; } 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); Cache::Add(filename, hash); return hash; } ON_ContentHash ON_ContentHash::CreateFromFile( const char* filename ) { if (auto pHash = Cache::FromFile(filename)) { return *pHash; } 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); Cache::Add(filename, hash); 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(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(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(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(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(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(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(m_full_path)); text_log.PushIndent(); text_log.Print("Relative path: \"%ls\"\n", static_cast(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(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; //https://mcneel.myjetbrains.com/youtrack/issue/RH-85181 //This used to check whether the file was there, eagerly, but this can be a big performance hit, especially on Google Drive. //And the accessor for this property is basically never called. m_full_path_status = ON_FileReference::Status::Unknown; } } } void ON_FileReference::SetFullPath( const char* full_path, bool bSetContentHash ) { const ON_wString local_full_path(full_path); SetFullPath(static_cast(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(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(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); } // ON_UnicodeTextFile class ON_File { public: virtual ~ON_File() { } bool Open(const wchar_t* filename, const wchar_t* mode) { _file = ON_FileStream::Open(filename, mode); return nullptr != _file; } bool Close(void) const { return ON_FileStream::Close(_file) == 0; } bool SeekFromCurrentPosition(ON__INT64 offset) const { return ON_FileStream::SeekFromCurrentPosition(_file, offset); } bool SeekFromStart(ON__INT64 offset) const { return ON_FileStream::SeekFromStart(_file, offset); } bool SeekFromEnd(ON__INT64 offset) const { return ON_FileStream::SeekFromEnd(_file, offset); } bool Seek(ON__INT64 offset, int origin) const { return ON_FileStream::Seek(_file, offset, origin); } ON__INT64 CurrentPosition(void) const { return ON_FileStream::CurrentPosition(_file); } ON__UINT64 Read(ON__UINT64 count, void* buffer) const { return ON_FileStream::Read(_file, count, buffer); } ON__UINT64 Write(ON__UINT64 count, const void* buffer) const { return ON_FileStream::Write(_file, count, buffer); } ON__UINT64 GetLength(void) const { const auto cur = CurrentPosition(); SeekFromEnd(0); const auto end = CurrentPosition(); SeekFromStart(cur); return end; } private: FILE* _file = nullptr; }; class ON_UnicodeTextFilePrivate final { public: ~ON_UnicodeTextFilePrivate() { Close(); } bool Open(const wchar_t* wszFullPath, ON_UnicodeTextFile::Modes mode); bool Close(void); bool ReadString(ON_wString& s); bool WriteString(const wchar_t* wsz); bool ReadHeader(ON_UnicodeTextFile::Types& t); bool WriteHeader(void); bool ReadStringFromUTF8(ON_wString& s); bool ReadStringFromUTF16(ON_wString& s); bool WriteStringToUTF8(const wchar_t* wsz); bool WriteStringToUTF16(const wchar_t* wsz); size_t ReadData(void* buf, size_t bytes_to_read); size_t WriteData(const void* buf, size_t bytes_to_write); public: ON_File _file; ON_UnicodeTextFile::Types _type = ON_UnicodeTextFile::Types::Unknown; }; size_t ON_UnicodeTextFilePrivate::ReadData(void* buf, size_t bytes_to_read) { return size_t(_file.Read(bytes_to_read, buf)); } size_t ON_UnicodeTextFilePrivate::WriteData(const void* buf, size_t bytes_to_write) { return size_t(_file.Write(bytes_to_write, buf)); } static const wchar_t* FileStreamMode(ON_UnicodeTextFile::Modes m) { if (m == ON_UnicodeTextFile::Modes::Read) return L"rb"; if (m == ON_UnicodeTextFile::Modes::Write) return L"wb"; ON_ASSERT(false); return L""; } bool ON_UnicodeTextFilePrivate::Open(const wchar_t* wszFullPath, ON_UnicodeTextFile::Modes mode) { bool ok = false; int attemptsCounter = 0; while (!ok && (attemptsCounter < 100)) { if (_file.Open(wszFullPath, FileStreamMode(mode))) { ok = true; } else { std::this_thread::sleep_for(std::chrono::milliseconds(100)); attemptsCounter++; } } if (ok) { if (ON_UnicodeTextFile::Modes::Write == mode) { ok = WriteHeader(); } else { ok = ReadHeader(_type); } } return ok; } bool ON_UnicodeTextFilePrivate::Close(void) { return _file.Close(); } bool ON_UnicodeTextFilePrivate::ReadHeader(ON_UnicodeTextFile::Types& t) { if (0 != _file.CurrentPosition()) return false; ON__UINT8 pBuf[3] = { 0 }; if (2 != ReadData(pBuf, 2)) return false; if (pBuf[0] == ON__UINT8(0xFF)) { if (pBuf[1] == ON__UINT8(0xFE)) { t = ON_UnicodeTextFile::Types::UTF16; return true; } } if (pBuf[0] == ON__UINT8(0xEF)) { if (pBuf[1] == ON__UINT8(0xBB)) { if (1 == ReadData(pBuf + 2, 1)) { if (pBuf[2] == ON__UINT8(0xBF)) { t = ON_UnicodeTextFile::Types::UTF8; return true; } } } } // No BOM was found so rewind and assume UTF8. This allows testing with ASCII files. _file.SeekFromStart(0); t = ON_UnicodeTextFile::Types::UTF8; return true; } bool ON_UnicodeTextFilePrivate::WriteHeader(void) { ON__UINT8 pBuf[3] = { 0 }; size_t sizeBOM = 2; if (ON_UnicodeTextFile::Types::UTF8 == _type) { sizeBOM = 3; pBuf[0] = ON__UINT8(0xEF); pBuf[1] = ON__UINT8(0xBB); pBuf[2] = ON__UINT8(0xBF); } else if (ON_UnicodeTextFile::Types::UTF16 == _type) { pBuf[0] = ON__UINT8(0xFF); pBuf[1] = ON__UINT8(0xFE); } else ON_ASSERT(false); // Did you forget to set the type in the constructor? if (!WriteData(pBuf, sizeBOM)) return false; return true; } bool ON_UnicodeTextFilePrivate::ReadString(ON_wString& s) { switch (_type) { case ON_UnicodeTextFile::Types::UTF8: return ReadStringFromUTF8(s); case ON_UnicodeTextFile::Types::UTF16: return ReadStringFromUTF16(s); case ON_UnicodeTextFile::Types::Unknown: default: return false; } } bool ON_UnicodeTextFilePrivate::WriteString(const wchar_t* wsz) { switch (_type) { case ON_UnicodeTextFile::Types::UTF8: return WriteStringToUTF8(wsz); case ON_UnicodeTextFile::Types::UTF16: return WriteStringToUTF16(wsz); case ON_UnicodeTextFile::Types::Unknown: default: return false; } } bool ON_UnicodeTextFilePrivate::ReadStringFromUTF8(ON_wString& s) { const auto size_in_bytes = size_t(_file.GetLength() - _file.CurrentPosition()); auto p = std::unique_ptr(new ON__UINT8[size_in_bytes + 1]); auto* pBuffer = p.get(); ReadData(pBuffer, size_in_bytes); pBuffer[size_in_bytes] = 0; const char* pUTF8 = reinterpret_cast(pBuffer); const auto num_chars = ON_ConvertUTF8ToWideChar(false, pUTF8, -1, nullptr, 0, nullptr, 0, 0, nullptr); auto* string_buf = s.SetLength(num_chars); if (nullptr == string_buf) return false; ON_ConvertUTF8ToWideChar(false, pUTF8, -1, string_buf, num_chars+1, nullptr, 0, 0, nullptr); return !s.IsEmpty(); } bool ON_UnicodeTextFilePrivate::ReadStringFromUTF16(ON_wString& s) { const auto char_size = sizeof(ON__UINT16); const auto size_in_bytes = size_t(_file.GetLength() - _file.CurrentPosition()); const auto size_in_chars = size_t(size_in_bytes / char_size); #ifdef ON_RUNTIME_WIN // On Windows, wchar_t is UTF16 so we can load the file directly into the ON_wString. ON_ASSERT(sizeof(wchar_t) == sizeof(ON__UINT16)); auto* buf = s.SetLength(size_in_chars); if (nullptr == buf) return false; if (ReadData(buf, size_in_bytes) != size_in_bytes) return false; buf[size_in_chars] = 0; #else // On Mac wchar_t is UTF32 so we have to load the file into a buffer and then convert it to the ON_wString. auto p = std::unique_ptr(new ON__UINT16[size_in_chars + 1]); auto* pUTF16 = p.get(); ReadData(pUTF16, size_in_bytes); pUTF16[size_in_chars] = 0; const auto num_chars = ON_ConvertUTF16ToUTF32(false, pUTF16, -1, nullptr, 0, nullptr, 0, 0, nullptr); auto* string_buf = s.SetLength(num_chars); if (nullptr == string_buf) return false; ON_ASSERT(sizeof(wchar_t) == sizeof(ON__UINT32)); auto* pWide = reinterpret_cast(string_buf); ON_ConvertUTF16ToUTF32(false, pUTF16, -1, pWide, num_chars+1, nullptr, 0, 0, nullptr); #endif return true; } bool ON_UnicodeTextFilePrivate::WriteStringToUTF8(const wchar_t* wsz) { const auto num_chars = ON_ConvertWideCharToUTF8(false, wsz, -1, nullptr, 0, nullptr, 0, 0, nullptr); auto p = std::unique_ptr(new char[size_t(num_chars) + 1]); auto* pBuffer = p.get(); ON_ConvertWideCharToUTF8(false, wsz, -1, pBuffer, num_chars + 1, nullptr, 0, 0, nullptr); if (WriteData(pBuffer, num_chars) != num_chars) return false; return true; } bool ON_UnicodeTextFilePrivate::WriteStringToUTF16(const wchar_t* wsz) { #ifdef ON_RUNTIME_WIN // On Windows, wchar_t is UTF16 so we can save the file directly from 'wsz'. ON_ASSERT(sizeof(wchar_t) == sizeof(ON__UINT16)); const auto size_in_bytes = wcslen(wsz) * sizeof(wchar_t); if (WriteData(wsz, size_in_bytes) != size_in_bytes) return false; #else // On Mac wchar_t is UTF32 so we have to convert 'wsz' to UTF16 in a buffer and write the buffer to the file. ON_ASSERT(sizeof(wchar_t) == sizeof(ON__UINT32)); auto* pWide = reinterpret_cast(wsz); const auto num_chars = ON_ConvertUTF32ToUTF16(false, pWide, -1, nullptr, 0, nullptr, 0, 0, nullptr); const auto num_chars_inc_term = num_chars + 1; auto p = std::unique_ptr(new ON__UINT16[num_chars_inc_term]); auto* pUTF16 = p.get(); ON_ConvertUTF32ToUTF16(false, pWide, -1, pUTF16, num_chars_inc_term, nullptr, 0, 0, nullptr); const auto size_in_bytes = num_chars * sizeof(ON__UINT16); if (WriteData(pUTF16, size_in_bytes) != size_in_bytes) return false; #endif return true; } ON_UnicodeTextFile::ON_UnicodeTextFile(Types t) { _private = new ON_UnicodeTextFilePrivate; _private->_type = t; } ON_UnicodeTextFile::~ON_UnicodeTextFile() { delete _private; } bool ON_UnicodeTextFile::Open(const wchar_t* wszFullPath, Modes mode) { return _private->Open(wszFullPath, mode); } bool ON_UnicodeTextFile::Close(void) { return _private->Close(); } bool ON_UnicodeTextFile::ReadString(ON_wString& s) { return _private->ReadString(s); } bool ON_UnicodeTextFile::WriteString(const wchar_t* wsz) { return _private->WriteString(wsz); }