From: Vadim Zeitlin Date: Mon, 15 Oct 2012 01:09:01 +0000 (+0000) Subject: Add support for symlinks to wxFileName. X-Git-Url: https://git.saurik.com/wxWidgets.git/commitdiff_plain/c063adebbad51e9ba8727a5e37d89190807864a4 Add support for symlinks to wxFileName. Allow to work with the symlinks themselves and not the file they reference by calling the new wxFileName::DontFollowLink(). Update Unix wxDir implementation to not treat symlinks to directories as directories, this ensures that we don't recurse into the directories outside of the original parent accidentally. Closes #14542. git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@72680 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775 --- diff --git a/docs/changes.txt b/docs/changes.txt index a5cb4aca19..a320e89782 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -529,6 +529,7 @@ Major new features in this release All: +- Add support for symlinks to wxFileName (David Hart). - Add separate read/written bytes counters and per-direction NOWAIT and WAITALL flags to wxSocket (Rob Bresalier). - Add wxDir::Close() method (Silverstorm82). diff --git a/include/wx/filename.h b/include/wx/filename.h index b351bf4d7e..8da9903cd1 100644 --- a/include/wx/filename.h +++ b/include/wx/filename.h @@ -132,13 +132,13 @@ public: // is contructed (the name will be empty), otherwise a file name and // extension are extracted from it wxFileName( const wxString& fullpath, wxPathFormat format = wxPATH_NATIVE ) - { Assign( fullpath, format ); } + { Assign( fullpath, format ); m_dontFollowLinks = false; } // from a directory name and a file name wxFileName(const wxString& path, const wxString& name, wxPathFormat format = wxPATH_NATIVE) - { Assign(path, name, format); } + { Assign(path, name, format); m_dontFollowLinks = false; } // from a volume, directory name, file base name and extension wxFileName(const wxString& volume, @@ -146,14 +146,14 @@ public: const wxString& name, const wxString& ext, wxPathFormat format = wxPATH_NATIVE) - { Assign(volume, path, name, ext, format); } + { Assign(volume, path, name, ext, format); m_dontFollowLinks = false; } // from a directory name, file base name and extension wxFileName(const wxString& path, const wxString& name, const wxString& ext, wxPathFormat format = wxPATH_NATIVE) - { Assign(path, name, ext, format); } + { Assign(path, name, ext, format); m_dontFollowLinks = false; } // the same for delayed initialization @@ -224,7 +224,7 @@ public: // does anything at all with this name (i.e. file, directory or some // other file system object such as a device, socket, ...) exist? - bool Exists() const { return Exists(GetFullPath()); } + bool Exists() const; static bool Exists(const wxString& path); @@ -367,6 +367,26 @@ public: { return Normalize(wxPATH_NORM_DOTS | wxPATH_NORM_ABSOLUTE | wxPATH_NORM_TILDE, cwd, format); } + + // If the path is a symbolic link (Unix-only), indicate that all + // filesystem operations on this path should be performed on the link + // itself and not on the file it points to, as is the case by default. + // + // No effect if this is not a symbolic link. + void DontFollowLink() + { + m_dontFollowLinks = true; + } + + // If the path is a symbolic link (Unix-only), returns whether various + // file operations should act on the link itself, or on its target. + // + // This does not test if the path is really a symlink or not. + bool ShouldFollowLink() const + { + return !m_dontFollowLinks; + } + #if defined(__WIN32__) && !defined(__WXWINCE__) && wxUSE_OLE // if the path is a shortcut, return the target and optionally, // the arguments @@ -606,6 +626,11 @@ private: // the difference is important as file with name "foo" and without // extension has full name "foo" while with empty extension it is "foo." bool m_hasExt; + + // by default, symlinks are dereferenced but this flag can be set with + // DontFollowLink() to change this and make different operations work on + // this file path itself instead of the target of the symlink + bool m_dontFollowLinks; }; #endif // _WX_FILENAME_H_ diff --git a/interface/wx/filename.h b/interface/wx/filename.h index 0c8171b033..cc6ccc9d5a 100644 --- a/interface/wx/filename.h +++ b/interface/wx/filename.h @@ -485,6 +485,27 @@ public: wxPathFormat format = wxPATH_NATIVE); /** + Turns off symlink dereferencing. + + By default, all operations in this class work on the target of a + symbolic link (symlink) if the path of the file is actually a symlink. + Using this method allows to turn off this "symlink following" behaviour + and apply the operations to this path itself, even if it is a symlink. + + The following methods are currently affected by this option: + - GetTimes() (but not SetTimes() as there is no portable way to + change the time of symlink itself). + - Existence checks: FileExists(), DirExists() and Exists() (notice + that static versions of these methods always follow symlinks). + - IsSameAs(). + + @see ShouldFollowLink() + + @since 2.9.5 + */ + void DontFollowLink(); + + /** Calls the static overload of this function with the full path of this object. @@ -1206,6 +1227,17 @@ public: */ void SetVolume(const wxString& volume); + /** + Return whether some operations will follow symlink. + + By default, file operations "follow symlink", i.e. operate on its + target and not on the symlink itself. See DontFollowLink() for more + information. + + @since 2.9.5 + */ + bool ShouldFollowLink() const; + //@{ /** This function splits a full file name into components: the volume (with the diff --git a/src/common/filename.cpp b/src/common/filename.cpp index f4d4beb7ad..349d00b7e3 100644 --- a/src/common/filename.cpp +++ b/src/common/filename.cpp @@ -332,6 +332,48 @@ static bool IsUNCPath(const wxString& path, wxPathFormat format) !IsDOSPathSep(path[2u]); } +#ifndef __WIN32__ + +// Under Unix-ish systems (basically everything except Windows) we may work +// either with the file itself or its target in case it's a symbolic link, as +// determined by wxFileName::ShouldFollowLink(). StatAny() can be used to stat +// the appropriate file with an extra twist that it also works (by following +// the symlink by default) when there is no wxFileName object at all, as is the +// case in static methods. + +// Private implementation, don't call directly, use one of the overloads below. +bool DoStatAny(wxStructStat& st, wxString path, const wxFileName* fn) +{ + // We need to remove any trailing slashes from the path because they could + // interfere with the symlink following decision: even if we use lstat(), + // it would still follow the symlink if we pass it a path with a slash at + // the end because the symlink resolution would happen while following the + // path and not for the last path element itself. + + while ( wxEndsWithPathSeparator(path) ) + path.RemoveLast(); + + int ret = !fn || fn->ShouldFollowLink() ? wxStat(path, &st) + : wxLstat(path, &st); + return ret == 0; +} + +// Overloads to use for a case when we don't have wxFileName object and when we +// do have one. +inline +bool StatAny(wxStructStat& st, const wxString& path) +{ + return DoStatAny(st, path, NULL); +} + +inline +bool StatAny(wxStructStat& st, const wxFileName& fn) +{ + return DoStatAny(st, fn.GetFullPath(), &fn); +} + +#endif // !__WIN32__ + // ---------------------------------------------------------------------------- // private constants // ---------------------------------------------------------------------------- @@ -357,6 +399,7 @@ void wxFileName::Assign( const wxFileName &filepath ) m_ext = filepath.GetExt(); m_relative = filepath.m_relative; m_hasExt = filepath.m_hasExt; + m_dontFollowLinks = filepath.m_dontFollowLinks; } void wxFileName::Assign(const wxString& volume, @@ -566,6 +609,9 @@ void wxFileName::Clear() // nor any extension m_hasExt = false; + + // follow symlinks by default + m_dontFollowLinks = false; } /* static */ @@ -623,8 +669,12 @@ void RemoveTrailingSeparatorsFromPath(wxString& strPath) #endif // __WINDOWS__ || __OS2__ -bool wxFileSystemObjectExists(const wxString& path, int flags) +bool +wxFileSystemObjectExists(const wxString& path, int flags, + const wxFileName* fn = NULL) { + wxUnusedVar(fn); // It's only used under Unix + // Should the existence of file/directory with this name be accepted, i.e. // result in the true return value from this function? const bool acceptFile = (flags & wxFileSystemObject_File) != 0; @@ -683,7 +733,7 @@ bool wxFileSystemObjectExists(const wxString& path, int flags) return false; #else // Non-MSW, non-OS/2 wxStructStat st; - if ( wxStat(strPath, &st) != 0 ) + if ( !DoStatAny(st, strPath, fn) ) return false; if ( S_ISREG(st.st_mode) ) @@ -699,7 +749,7 @@ bool wxFileSystemObjectExists(const wxString& path, int flags) bool wxFileName::FileExists() const { - return wxFileName::FileExists( GetFullPath() ); + return wxFileSystemObjectExists(GetFullPath(), wxFileSystemObject_File, this); } /* static */ @@ -710,7 +760,7 @@ bool wxFileName::FileExists( const wxString &filePath ) bool wxFileName::DirExists() const { - return wxFileName::DirExists( GetPath() ); + return wxFileSystemObjectExists(GetPath(), wxFileSystemObject_Dir, this); } /* static */ @@ -719,6 +769,11 @@ bool wxFileName::DirExists( const wxString &dirPath ) return wxFileSystemObjectExists(dirPath, wxFileSystemObject_Dir); } +bool wxFileName::Exists() const +{ + return wxFileSystemObjectExists(GetFullPath(), wxFileSystemObject_Any, this); +} + /* static */ bool wxFileName::Exists(const wxString& path) { @@ -1779,8 +1834,7 @@ bool wxFileName::SameAs(const wxFileName& filepath, wxPathFormat format) const #if defined(__UNIX__) wxStructStat st1, st2; - if ( wxStat(fn1.GetFullPath(), &st1) == 0 && - wxStat(fn2.GetFullPath(), &st2) == 0 ) + if ( StatAny(st1, fn1) && StatAny(st2, fn2) ) { if ( st1.st_ino == st2.st_ino && st1.st_dev == st2.st_dev ) return true; @@ -2647,7 +2701,7 @@ bool wxFileName::GetTimes(wxDateTime *dtAccess, #elif defined(__UNIX_LIKE__) || defined(__WXMAC__) || defined(__OS2__) || (defined(__DOS__) && defined(__WATCOMC__)) // no need to test for IsDir() here wxStructStat stBuf; - if ( wxStat( GetFullPath(), &stBuf) == 0 ) + if ( StatAny(stBuf, *this) ) { // Android defines st_*time fields as unsigned long, but time_t as long, // hence the static_casts. diff --git a/src/unix/dir.cpp b/src/unix/dir.cpp index f274a208d1..f0e6cd9197 100644 --- a/src/unix/dir.cpp +++ b/src/unix/dir.cpp @@ -31,6 +31,7 @@ #include "wx/dir.h" #include "wx/filefn.h" // for wxMatchWild +#include "wx/filename.h" #include #include @@ -149,13 +150,17 @@ bool wxDirData::Read(wxString *filename) break; } - // check the type now - if ( !(m_flags & wxDIR_FILES) && !wxDir::Exists(path + de_d_name) ) + // check the type now: notice that we want to check the type of this + // path itself and not whatever it points to in case of a symlink + wxFileName fn = wxFileName::DirName(path + de_d_name); + fn.DontFollowLink(); + + if ( !(m_flags & wxDIR_FILES) && !fn.DirExists() ) { // it's a file, but we don't want them continue; } - else if ( !(m_flags & wxDIR_DIRS) && wxDir::Exists(path + de_d_name) ) + else if ( !(m_flags & wxDIR_DIRS) && fn.DirExists() ) { // it's a dir, and we don't want it continue; diff --git a/tests/filename/filenametest.cpp b/tests/filename/filenametest.cpp index 638434bc56..5cd5ed98e7 100644 --- a/tests/filename/filenametest.cpp +++ b/tests/filename/filenametest.cpp @@ -141,6 +141,9 @@ private: CPPUNIT_TEST( TestGetTimes ); CPPUNIT_TEST( TestExists ); CPPUNIT_TEST( TestIsSame ); +#if defined(__UNIX__) + CPPUNIT_TEST( TestSymlinks ); +#endif // __UNIX__ CPPUNIT_TEST_SUITE_END(); void TestConstruction(); @@ -160,6 +163,9 @@ private: void TestGetTimes(); void TestExists(); void TestIsSame(); +#if defined(__UNIX__) + void TestSymlinks(); +#endif // __UNIX__ DECLARE_NO_COPY_CLASS(FileNameTestCase) }; @@ -727,3 +733,133 @@ void FileNameTestCase::TestIsSame() CPPUNIT_ASSERT( fn3.SameAs(fn4) ); #endif // __UNIX__ } + +#if defined(__UNIX__) + +// Tests for functions that are changed by ShouldFollowLink() +void FileNameTestCase::TestSymlinks() +{ + const wxString tmpdir(wxStandardPaths::Get().GetTempDir()); + + wxFileName tmpfn(wxFileName::DirName(tmpdir)); + + wxDateTime dtAccessTmp, dtModTmp, dtCreateTmp; + CPPUNIT_ASSERT(tmpfn.GetTimes(&dtAccessTmp, &dtModTmp, &dtCreateTmp)); + + // Create a temporary directory + wxString name = tmpdir + "/filenametestXXXXXX"; + wxString tempdir = wxString::From8BitData(mkdtemp(name.char_str())); + tempdir << wxFileName::GetPathSeparator(); + wxFileName tempdirfn(wxFileName::DirName(tempdir)); + CPPUNIT_ASSERT(tempdirfn.DirExists()); + + // Create a regular file in that dir, to act as a symlink target + wxFileName targetfn(wxFileName::CreateTempFileName(tempdir)); + CPPUNIT_ASSERT(targetfn.FileExists()); + + // Create a symlink to that file, and another to the home dir + wxFileName linktofile(tempdir, "linktofile"); + CPPUNIT_ASSERT_EQUAL(0, symlink(targetfn.GetFullPath().c_str(), + linktofile.GetFullPath().c_str())); + + const wxString linktodirName(tempdir + "/linktodir"); + wxFileName linktodir(wxFileName::DirName(linktodirName)); + CPPUNIT_ASSERT_EQUAL(0, symlink(tmpfn.GetFullPath().c_str(), + linktodirName.c_str())); + + // And symlinks to both of those symlinks + wxFileName linktofilelnk(tempdir, "linktofilelnk"); + CPPUNIT_ASSERT_EQUAL(0, symlink(linktofile.GetFullPath().c_str(), + linktofilelnk.GetFullPath().c_str())); + wxFileName linktodirlnk(tempdir, "linktodirlnk"); + CPPUNIT_ASSERT_EQUAL(0, symlink(linktodir.GetFullPath().c_str(), + linktodirlnk.GetFullPath().c_str())); + + // Run the tests twice: once in the default symlink following mode and the + // second time without following symlinks. + bool deref = true; + for ( int n = 0; n < 2; ++n, deref = !deref ) + { + const std::string msg(deref ? " failed for the link target" + : " failed for the path itself"); + + if ( !deref ) + { + linktofile.DontFollowLink(); + linktodir.DontFollowLink(); + linktofilelnk.DontFollowLink(); + linktodirlnk.DontFollowLink(); + } + + // Test SameAs() + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Comparison with file" + msg, + deref, linktofile.SameAs(targetfn) + ); + + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Comparison with directory" + msg, + deref, linktodir.SameAs(tmpfn) + ); + + // A link-to-a-link should dereference through to the final target + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Comparison with link to a file" + msg, + deref, + linktofilelnk.SameAs(targetfn) + ); + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Comparison with link to a directory" + msg, + deref, + linktodirlnk.SameAs(tmpfn) + ); + + // Test GetTimes() + wxDateTime dtAccess, dtMod, dtCreate; + CPPUNIT_ASSERT_MESSAGE + ( + "Getting times of a directory" + msg, + linktodir.GetTimes(&dtAccess, &dtMod, &dtCreate) + ); + + // IsEqualTo() should be true only when dereferencing. Don't test each + // individually: accessing to create the link will have updated some + bool equal = dtCreate.IsEqualTo(dtCreateTmp) && + dtMod.IsEqualTo(dtModTmp) && + dtAccess.IsEqualTo(dtAccessTmp); + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Comparing directory times" + msg, + deref, + equal + ); + + // Test Exists() + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Testing file existence" + msg, + deref, + linktofile.FileExists() + ); + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Testing directory existence" + msg, + deref, + linktodir.DirExists() + ); + } + + // Finally test Exists() after removing the file. + CPPUNIT_ASSERT(wxRemoveFile(targetfn.GetFullPath())); + CPPUNIT_ASSERT(!wxFileName(tempdir, "linktofile").Exists()); + CPPUNIT_ASSERT(linktofile.Exists()); + + // Clean-up, and also tests removal of a dir containing a symlink-to-dir + CPPUNIT_ASSERT(tempdirfn.Rmdir(wxPATH_RMDIR_RECURSIVE)); +} + +#endif // __UNIX__