diff --git a/include/wx/filefn.h b/include/wx/filefn.h index 13cc62c253..e975ee0022 100644 --- a/include/wx/filefn.h +++ b/include/wx/filefn.h @@ -381,7 +381,10 @@ enum wxPosixPermissions #define wxCRT_Access access #define wxCRT_Chmod chmod + #define wxCRT_Readlink readlink + #define wxHAS_NATIVE_LSTAT + #define wxHAS_NATIVE_READLINK #endif // platforms // if the platform doesn't have symlinks, define wxCRT_Lstat to be the same as @@ -406,6 +409,11 @@ inline int wxChmod(const wxString& path, mode_t mode) inline int wxOpen(const wxString& path, int flags, mode_t mode) { return wxCRT_Open(path.fn_str(), flags, mode); } +#if defined(wxHAS_NATIVE_READLINK) +inline int wxReadlink(const wxString& path, char* buf, int size) + { return wxCRT_Readlink(path.fn_str(), buf, size); } +#endif + inline int wxStat(const wxString& path, wxStructStat *buf) { return wxCRT_Stat(path.fn_str(), buf); } inline int wxLstat(const wxString& path, wxStructStat *buf) diff --git a/include/wx/filename.h b/include/wx/filename.h index 25ed41977b..1a51030b89 100644 --- a/include/wx/filename.h +++ b/include/wx/filename.h @@ -389,6 +389,9 @@ public: return !m_dontFollowLinks; } + // Resolve a wxFileName object representing a link to its target + wxFileName ResolveLink(); + #if defined(__WIN32__) && wxUSE_OLE // if the path is a shortcut, return the target and optionally, // the arguments diff --git a/interface/wx/filename.h b/interface/wx/filename.h index 4931050faf..f3bbcea62e 100644 --- a/interface/wx/filename.h +++ b/interface/wx/filename.h @@ -1138,6 +1138,24 @@ public: */ bool ReplaceHomeDir(wxPathFormat format = wxPATH_NATIVE); + /** + Find the absolute path of the file/directory that is pointed to by this + path. + + If this path isn't a symlink, then this function will return the current + path. If the path does not exist on disk, An empty wxFileName instance + will be returned. + + @note This is only supported on Unix-like platforms (e.g. wxGTK, wxOSX), + on other platforms (e.g. wxMSW) this function just returns the + current path. + + @since 3.1.5 + + @return The absolute path that the current symlink path points to. + */ + wxFileName ResolveLink(); + /** Deletes the specified directory from the file system. diff --git a/src/common/filename.cpp b/src/common/filename.cpp index d6842e69b0..9d1d6b0253 100644 --- a/src/common/filename.cpp +++ b/src/common/filename.cpp @@ -1668,6 +1668,56 @@ bool wxFileName::GetShortcutTarget(const wxString& shortcutPath, #endif // __WIN32__ +// ---------------------------------------------------------------------------- +// Resolve links +// ---------------------------------------------------------------------------- + +wxFileName wxFileName::ResolveLink() +{ + wxString link = GetFullPath(); + wxFileName linkTarget( *this ); + +// Only resolve links on platforms with readlink (e.g. Unix-like platforms) +#if defined(wxHAS_NATIVE_READLINK) + wxStructStat st; + + // This means the link itself doesn't exist, so return an empty filename + if ( !StatAny(st, link, wxFILE_EXISTS_NO_FOLLOW) ) + { + linkTarget.Clear(); + return linkTarget; + } + + // If it isn't an actual link, bail out just and return the link as the result + if ( !S_ISLNK(st.st_mode) ) + return linkTarget; + + // Dynamically compute the buffer size from the stat call, but fallback if it isn't valid + int bufSize = 4096; + if( st.st_size != 0 ) + bufSize = st.st_size + 1; + + char buf[bufSize]; + int result = wxReadlink(link, buf, bufSize - 1); + + if ( result != -1 ) + { + buf[result] = '\0'; // readlink() doesn't NULL-terminate the buffer + linkTarget.Assign( wxString(buf, wxConvLibc) ); + + // Ensure the resulting path is absolute since readlink can return paths relative to the link + if ( !linkTarget.IsAbsolute() ) + linkTarget.MakeAbsolute(GetPath()); + } + else + { + // This means the lookup failed for some reason + linkTarget.Clear(); + } +#endif + + return linkTarget; +} // ---------------------------------------------------------------------------- // absolute/relative paths diff --git a/tests/filename/filenametest.cpp b/tests/filename/filenametest.cpp index f16f931664..57b81680dc 100644 --- a/tests/filename/filenametest.cpp +++ b/tests/filename/filenametest.cpp @@ -842,25 +842,71 @@ void FileNameTestCase::TestSymlinks() wxFileName targetfn(wxFileName::CreateTempFileName(tempdir)); CPPUNIT_ASSERT(targetfn.FileExists()); + // Resolving a non-symlink will just return the same thing + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Non-symlink didn't resolve to the same file", + targetfn, targetfn.ResolveLink() + ); + // Create a symlink to that file wxFileName linktofile(tempdir, "linktofile"); CPPUNIT_ASSERT_EQUAL(0, symlink(targetfn.GetFullPath().c_str(), - linktofile.GetFullPath().c_str())); + linktofile.GetFullPath().c_str())); + + // Test resolving the filename to the symlink + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Failed to resolve symlink to file", + targetfn, linktofile.ResolveLink() + ); + + // Create a relative symlink to that file + wxFileName relativelinktofile(tempdir, "relativelinktofile"); + wxString relativeFileName = "./" + targetfn.GetFullName(); + CPPUNIT_ASSERT_EQUAL(0, symlink(relativeFileName.c_str(), + relativelinktofile.GetFullPath().c_str())); + + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Failed to resolve relative symlink to absolute file path", + targetfn, relativelinktofile.ResolveLink() + ); // ... and another to the temporary directory const wxString linktodirName(tempdir + "/linktodir"); + wxFileName linktodirfn(linktodirName); wxFileName linktodir(wxFileName::DirName(linktodirName)); CPPUNIT_ASSERT_EQUAL(0, symlink(tmpfn.GetFullPath().c_str(), - linktodirName.c_str())); + linktodirName.c_str())); + + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Failed to resolve symlink to directory", + tmpfn, linktodirfn.ResolveLink() + ); // And symlinks to both of those symlinks wxFileName linktofilelnk(tempdir, "linktofilelnk"); CPPUNIT_ASSERT_EQUAL(0, symlink(linktofile.GetFullPath().c_str(), linktofilelnk.GetFullPath().c_str())); + + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Failed to resolve symlink to file symlink", + targetfn, linktofilelnk.ResolveLink() + ); + wxFileName linktodirlnk(tempdir, "linktodirlnk"); CPPUNIT_ASSERT_EQUAL(0, symlink(linktodir.GetFullPath().c_str(), linktodirlnk.GetFullPath().c_str())); + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Failed to resolve symlink to directory symlink", + tmpfn, linktodirlnk.ResolveLink() + ); + // Run the tests twice: once in the default symlink following mode and the // second time without following symlinks. bool deref = true; @@ -969,6 +1015,14 @@ void FileNameTestCase::TestSymlinks() // Finally test Exists() after removing the file. CPPUNIT_ASSERT(wxRemoveFile(targetfn.GetFullPath())); + + // Resolving a file that doesn't exist returns empty + CPPUNIT_ASSERT_EQUAL_MESSAGE + ( + "Non-existant file didn't resolve correctly", + wxFileName(), targetfn.ResolveLink() + ); + // This should succeed, as the symlink still exists and // the default wxFILE_EXISTS_ANY implies wxFILE_EXISTS_NO_FOLLOW CPPUNIT_ASSERT(wxFileName(tempdir, "linktofile").Exists());