|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Enumeration;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace System.IO
{
/// <summary>Provides an implementation of FileSystem for Unix systems.</summary>
internal static partial class FileSystem
{
// On Linux, the maximum number of symbolic links that are followed while resolving a pathname is 40.
// See: https://man7.org/linux/man-pages/man7/path_resolution.7.html
private const int MaxFollowedLinks = 40;
// This gets filtered by umask.
internal const UnixFileMode DefaultUnixCreateDirectoryMode =
UnixFileMode.UserRead |
UnixFileMode.UserWrite |
UnixFileMode.UserExecute |
UnixFileMode.GroupRead |
UnixFileMode.GroupWrite |
UnixFileMode.GroupExecute |
UnixFileMode.OtherRead |
UnixFileMode.OtherWrite |
UnixFileMode.OtherExecute;
static partial void TryCloneFile(string sourceFullPath, string destFullPath, bool overwrite, ref bool cloned);
public static void CopyFile(string sourceFullPath, string destFullPath, bool overwrite)
{
long fileLength;
UnixFileMode filePermissions;
using SafeFileHandle src = SafeFileHandle.OpenReadOnly(sourceFullPath, FileOptions.None, out fileLength, out filePermissions);
// Try to clone the file first.
bool cloned = false;
TryCloneFile(sourceFullPath, destFullPath, overwrite, ref cloned);
if (cloned)
{
return;
}
using SafeFileHandle dst = SafeFileHandle.Open(destFullPath, overwrite ? FileMode.Create : FileMode.CreateNew,
FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize: 0, filePermissions,
CreateOpenExceptionForCopyFile);
Interop.CheckIo(Interop.Sys.CopyFile(src, dst, fileLength));
}
private static Exception? CreateOpenExceptionForCopyFile(Interop.ErrorInfo error, Interop.Sys.OpenFlags flags, string path)
{
// If the destination path points to a directory, we throw to match Windows behaviour.
if (error.Error == Interop.Error.EEXIST && DirectoryExists(path))
{
return new IOException(SR.Format(SR.Arg_FileIsDirectory_Name, path));
}
return null; // Let SafeFileHandle create the exception for this error.
}
#pragma warning disable IDE0060
public static void Encrypt(string path)
{
throw new PlatformNotSupportedException(SR.PlatformNotSupported_FileEncryption);
}
public static void Decrypt(string path)
{
throw new PlatformNotSupportedException(SR.PlatformNotSupported_FileEncryption);
}
#pragma warning restore IDE0060
private static void LinkOrCopyFile (string sourceFullPath, string destFullPath)
{
if (Interop.Sys.Link(sourceFullPath, destFullPath) >= 0)
return;
// If link fails, we can fall back to doing a full copy, but we'll only do so for
// cases where we expect link could fail but such a copy could succeed. We don't
// want to do so for all errors, because the copy could incur a lot of cost
// even if we know it'll eventually fail, e.g. EROFS means that the source file
// system is read-only and couldn't support the link being added, but if it's
// read-only, then the move should fail any way due to an inability to delete
// the source file.
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
if (errorInfo.Error == Interop.Error.EXDEV || // rename fails across devices / mount points
errorInfo.Error == Interop.Error.EACCES ||
errorInfo.Error == Interop.Error.EPERM || // permissions might not allow creating hard links even if a copy would work
errorInfo.Error == Interop.Error.EOPNOTSUPP || // links aren't supported by the source file system
errorInfo.Error == Interop.Error.EMLINK || // too many hard links to the source file
errorInfo.Error == Interop.Error.ENOSYS) // the file system doesn't support link
{
CopyFile(sourceFullPath, destFullPath, overwrite: false);
}
else
{
// The operation failed. Within reason, try to determine which path caused the problem
// so we can throw a detailed exception.
if (errorInfo.Error == Interop.Error.ENOENT)
{
if (!Directory.Exists(Path.GetDirectoryName(destFullPath)))
{
throw Interop.GetExceptionForIoErrno(errorInfo, destFullPath, isDirError: true);
}
else
{
throw Interop.GetExceptionForIoErrno(errorInfo, sourceFullPath);
}
}
else if (errorInfo.Error == Interop.Error.EEXIST)
{
throw Interop.GetExceptionForIoErrno(errorInfo, destFullPath);
}
throw Interop.GetExceptionForIoErrno(errorInfo);
}
}
#pragma warning disable IDE0060
public static void ReplaceFile(string sourceFullPath, string destFullPath, string? destBackupFullPath, bool ignoreMetadataErrors /* unused */)
{
// Unix rename works in more cases, we limit to what is allowed by Windows File.Replace.
// These checks are not atomic, the file could change after a check was performed and before it is renamed.
Interop.CheckIo(Interop.Sys.LStat(sourceFullPath, out Interop.Sys.FileStatus sourceStat), sourceFullPath);
// Check source is not a directory.
if ((sourceStat.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR)
{
throw new UnauthorizedAccessException(SR.Format(SR.IO_NotAFile, sourceFullPath));
}
Interop.Sys.FileStatus destStat;
if (Interop.Sys.LStat(destFullPath, out destStat) == 0)
{
// Check destination is not a directory.
if ((destStat.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR)
{
throw new UnauthorizedAccessException(SR.Format(SR.IO_NotAFile, destFullPath));
}
// Check source and destination are not the same.
if (sourceStat.Dev == destStat.Dev &&
sourceStat.Ino == destStat.Ino)
{
throw new IOException(SR.Format(SR.IO_CannotReplaceSameFile, sourceFullPath, destFullPath));
}
}
if (destBackupFullPath != null)
{
// We're backing up the destination file to the backup file, so we need to first delete the backup
// file, if it exists. If deletion fails for a reason other than the file not existing, fail.
if (Interop.Sys.Unlink(destBackupFullPath) != 0)
{
Interop.ErrorInfo errno = Interop.Sys.GetLastErrorInfo();
if (errno.Error != Interop.Error.ENOENT)
{
throw Interop.GetExceptionForIoErrno(errno, destBackupFullPath);
}
}
// Now that the backup is gone, link the backup to point to the same file as destination.
// This way, we don't lose any data in the destination file, no copy is necessary, etc.
LinkOrCopyFile(destFullPath, destBackupFullPath);
}
else
{
// There is no backup file. Just make sure the destination file exists, throwing if it doesn't.
if (Interop.Sys.Stat(destFullPath, out _) != 0)
{
Interop.ErrorInfo errno = Interop.Sys.GetLastErrorInfo();
if (errno.Error == Interop.Error.ENOENT)
{
throw Interop.GetExceptionForIoErrno(errno, destBackupFullPath);
}
}
}
// Finally, rename the source to the destination, overwriting the destination.
Interop.CheckIo(Interop.Sys.Rename(sourceFullPath, destFullPath));
}
#pragma warning restore IDE0060
public static void MoveFile(string sourceFullPath, string destFullPath)
{
MoveFile(sourceFullPath, destFullPath, false);
}
public static void MoveFile(string sourceFullPath, string destFullPath, bool overwrite)
{
// If overwrite is allowed then just call rename
if (overwrite)
{
if (Interop.Sys.Rename(sourceFullPath, destFullPath) < 0)
{
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
if (errorInfo.Error == Interop.Error.EXDEV) // rename fails across devices / mount points
{
CopyFile(sourceFullPath, destFullPath, overwrite);
DeleteFile(sourceFullPath);
}
else
{
throw Interop.GetExceptionForIoErrno(errorInfo, destFullPath);
}
}
// Rename or CopyFile complete
return;
}
// The desired behavior for Move(source, dest) is to not overwrite the destination file
// if it exists. Since rename(source, dest) will replace the file at 'dest' if it exists,
// link/unlink are used instead. Rename is more efficient than link/unlink on file systems
// where hard links are not supported (such as FAT). Therefore, given that source file exists,
// rename is used in 2 cases: when dest file does not exist or when source path and dest
// path refer to the same file (on the same device). This is important for case-insensitive
// file systems (e.g. renaming a file in a way that just changes casing), so that we support
// changing the casing in the naming of the file. If this fails in any way (e.g. source file
// doesn't exist, dest file doesn't exist, rename fails, etc.), we just fall back to trying the
// link/unlink approach and generating any exceptional messages from there as necessary.
Interop.Sys.FileStatus sourceStat, destStat;
if (Interop.Sys.LStat(sourceFullPath, out sourceStat) == 0 && // source file exists
(Interop.Sys.LStat(destFullPath, out destStat) != 0 || // dest file does not exist
(sourceStat.Dev == destStat.Dev && // source and dest are on the same device
sourceStat.Ino == destStat.Ino)) && // source and dest are the same file on that device
Interop.Sys.Rename(sourceFullPath, destFullPath) == 0) // try the rename
{
// Renamed successfully.
return;
}
LinkOrCopyFile(sourceFullPath, destFullPath);
DeleteFile(sourceFullPath);
}
public static void DeleteFile(string fullPath)
{
if (Interop.Sys.Unlink(fullPath) < 0)
{
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
switch (errorInfo.Error)
{
case Interop.Error.ENOENT:
// In order to match Windows behavior
string? directoryName = Path.GetDirectoryName(fullPath);
Debug.Assert(directoryName != null);
if (directoryName.Length > 0 && !Directory.Exists(directoryName))
{
throw Interop.GetExceptionForIoErrno(errorInfo, fullPath, true);
}
return;
case Interop.Error.EROFS:
// EROFS means the file system is read-only
// Need to manually check file existence
// https://github.com/dotnet/runtime/issues/22382
Interop.ErrorInfo fileExistsError;
// Input allows trailing separators in order to match Windows behavior
// Unix does not accept trailing separators, so must be trimmed
if (!FileExists(fullPath, out fileExistsError) &&
fileExistsError.Error == Interop.Error.ENOENT)
{
return;
}
goto default;
case Interop.Error.EISDIR:
errorInfo = Interop.Error.EACCES.Info();
goto default;
default:
throw Interop.GetExceptionForIoErrno(errorInfo, fullPath);
}
}
}
public static void CreateDirectory(string fullPath)
=> CreateDirectory(fullPath, DefaultUnixCreateDirectoryMode);
public static void CreateDirectory(string fullPath, UnixFileMode unixCreateMode)
{
// The argument is a full path, which means it is an absolute path that
// doesn't contain "//", "/./", and "/../".
Debug.Assert(fullPath.Length > 0);
Debug.Assert(PathInternal.IsDirectorySeparator(fullPath[0]));
if (fullPath.Length == 1)
{
return; // fullPath is '/'.
}
// macOS returns ENOTDIR when the path refers to a file and ends with '/'.
// Trim the separator so we get EEXIST instead.
ReadOnlySpan<char> path = PathInternal.TrimEndingDirectorySeparator(fullPath.AsSpan());
int result = Interop.Sys.MkDir(path, (int)unixCreateMode);
if (result == 0)
{
return; // Created directory.
}
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
if (errorInfo.Error == Interop.Error.EEXIST && DirectoryExists(fullPath))
{
return; // Path already exists and it's a directory.
}
else if (errorInfo.Error == Interop.Error.ENOENT) // Some parts of the path don't exist yet.
{
CreateParentsAndDirectory(fullPath, unixCreateMode);
}
else
{
throw Interop.GetExceptionForIoErrno(errorInfo, fullPath);
}
}
private static void CreateParentsAndDirectory(string fullPath, UnixFileMode unixCreateMode)
{
// Try create parents bottom to top and track those that could not
// be created due to missing parents. Then create them top to bottom.
using ValueListBuilder<int> stackDir = new(stackalloc int[32]); // 32 arbitrarily chosen
stackDir.Append(fullPath.Length);
int i = fullPath.Length - 1;
if (PathInternal.IsDirectorySeparator(fullPath[i]))
{
i--; // Trim trailing separator.
}
do
{
// Find the end of the parent directory.
Debug.Assert(!PathInternal.IsDirectorySeparator(fullPath[i]));
while (!PathInternal.IsDirectorySeparator(fullPath[i]))
{
i--;
}
ReadOnlySpan<char> mkdirPath = fullPath.AsSpan(0, i);
int result = Interop.Sys.MkDir(mkdirPath, (int)DefaultUnixCreateDirectoryMode);
if (result == 0)
{
break; // Created parent.
}
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
if (errorInfo.Error == Interop.Error.ENOENT)
{
// Some parts of the path don't exist yet.
// We'll try to create its parent on the next iteration.
// Track this path for later creation.
stackDir.Append(mkdirPath.Length);
}
else if (errorInfo.Error == Interop.Error.EEXIST)
{
// Parent exists.
// If it is not a directory, MkDir will fail when we create a child directory.
break;
}
else
{
throw Interop.GetExceptionForIoErrno(errorInfo, mkdirPath.ToString());
}
i--;
} while (i > 0);
// Create directories that had missing parents.
for (i = stackDir.Length - 1; i >= 0; i--)
{
ReadOnlySpan<char> mkdirPath = fullPath.AsSpan(0, stackDir[i]);
UnixFileMode mode = i == 0 ? unixCreateMode : DefaultUnixCreateDirectoryMode;
int result = Interop.Sys.MkDir(mkdirPath, (int)mode);
if (result < 0)
{
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
if (errorInfo.Error == Interop.Error.EEXIST)
{
// Path was created since we last checked.
// Continue, and for the last item, which is fullPath,
// verify it is actually a directory.
if (i != 0)
{
continue;
}
if (DirectoryExists(mkdirPath))
{
return;
}
}
throw Interop.GetExceptionForIoErrno(errorInfo, mkdirPath.ToString());
}
}
}
private static void MoveDirectory(string sourceFullPath, string destFullPath, bool isCaseSensitiveRename)
{
// isCaseSensitiveRename is only set for case-insensitive systems (like macOS).
Debug.Assert(!isCaseSensitiveRename || !PathInternal.IsCaseSensitive);
ReadOnlySpan<char> srcNoDirectorySeparator = Path.TrimEndingDirectorySeparator(sourceFullPath.AsSpan());
ReadOnlySpan<char> destNoDirectorySeparator = Path.TrimEndingDirectorySeparator(destFullPath.AsSpan());
// When the path ends with a directory separator, it must not be a file.
// On Unix 'rename' fails with ENOTDIR, on wasm we need to manually check.
if (OperatingSystem.IsBrowser() && Path.EndsInDirectorySeparator(sourceFullPath) && FileExists(sourceFullPath))
{
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath));
}
// The destination must not exist (unless it is a case-sensitive rename).
// On Unix 'rename' will overwrite the destination file if it already exists, we need to manually check.
if (!isCaseSensitiveRename && Interop.Sys.LStat(destNoDirectorySeparator, out Interop.Sys.FileStatus destFileStatus) >= 0)
{
// Maintain order of exceptions as on Windows.
// Throw if the source doesn't exist.
if (Interop.Sys.LStat(srcNoDirectorySeparator, out Interop.Sys.FileStatus sourceFileStatus) < 0)
{
throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath));
}
// Source and destination must not be the same file unless it is a case-sensitive rename.
else if (sourceFileStatus.Dev == destFileStatus.Dev &&
sourceFileStatus.Ino == destFileStatus.Ino)
{
// isCaseSensitiveRename is only true when the system is case-insensitive (like macOS).
// On a case-sensitive system (like Linux), there can stil be case-insensitive filesystems mounted.
// When both paths refer to the same file and they differ only in casing, we fall through to Rename.
if (!PathInternal.IsCaseSensitive && // handled by isCaseSensitiveRename.
!srcNoDirectorySeparator.Equals(destNoDirectorySeparator, StringComparison.OrdinalIgnoreCase) || // different paths.
Path.GetFileName(srcNoDirectorySeparator).SequenceEqual(Path.GetFileName(destNoDirectorySeparator))) // same names.
{
throw new IOException(SR.IO_SourceDestMustBeDifferent);
}
}
// When the path ends with a directory separator, it must be a directory.
else if ((sourceFileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) != Interop.Sys.FileTypes.S_IFDIR
&& Path.EndsInDirectorySeparator(sourceFullPath))
{
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath));
}
else
{
throw new IOException(SR.Format(SR.IO_AlreadyExists_Name, destFullPath));
}
}
if (Interop.Sys.Rename(sourceFullPath, destNoDirectorySeparator) < 0)
{
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
switch (errorInfo.Error)
{
case Interop.Error.EACCES: // match Win32 exception
throw new IOException(SR.Format(SR.UnauthorizedAccess_IODenied_Path, sourceFullPath), errorInfo.RawErrno);
case Interop.Error.ENOENT:
throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath));
case Interop.Error.ENOTDIR: // sourceFullPath exists and it's not a directory
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath));
default:
throw Interop.GetExceptionForIoErrno(errorInfo);
}
}
}
public static void RemoveDirectory(string fullPath, bool recursive)
{
// Delete the directory.
// If we're recursing, don't throw when it is not empty, and perform a recursive remove.
if (!RemoveEmptyDirectory(fullPath, topLevel: true, throwWhenNotEmpty: !recursive))
{
Debug.Assert(recursive);
RemoveDirectoryRecursive(fullPath);
}
}
private static void RemoveDirectoryRecursive(string fullPath)
{
Exception? firstException = null;
try
{
var fse = new FileSystemEnumerable<(string, bool)>(fullPath,
static (ref FileSystemEntry entry) =>
{
// Don't report symlinks to directories as directories.
bool isRealDirectory = !entry.IsSymbolicLink && entry.IsDirectory;
return (entry.ToFullPath(), isRealDirectory);
},
EnumerationOptions.Compatible);
foreach ((string childPath, bool isDirectory) in fse)
{
try
{
if (isDirectory)
{
RemoveDirectoryRecursive(childPath);
}
else
{
DeleteFile(childPath);
}
}
catch (Exception ex)
{
firstException ??= ex;
}
}
}
catch (Exception exc)
{
firstException ??= exc;
}
if (firstException != null)
{
throw firstException;
}
RemoveEmptyDirectory(fullPath);
}
private static bool RemoveEmptyDirectory(string fullPath, bool topLevel = false, bool throwWhenNotEmpty = true)
{
if (Interop.Sys.RmDir(fullPath) < 0)
{
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
if (errorInfo.Error == Interop.Error.ENOTEMPTY)
{
if (!throwWhenNotEmpty)
{
return false;
}
}
else if (errorInfo.Error == Interop.Error.ENOENT)
{
// When we're recursing, don't throw for items that go missing.
if (!topLevel)
{
return true;
}
}
else if (DirectoryExists(fullPath, out Interop.ErrorInfo existErr))
{
// Top-level path is a symlink to a directory, delete the link.
if (topLevel && errorInfo.Error == Interop.Error.ENOTDIR)
{
DeleteFile(fullPath);
return true;
}
}
else if (existErr.Error == Interop.Error.ENOENT)
{
// Prefer throwing DirectoryNotFoundException over other exceptions.
errorInfo = existErr;
}
if (errorInfo.Error == Interop.Error.EACCES ||
errorInfo.Error == Interop.Error.EPERM ||
errorInfo.Error == Interop.Error.EROFS)
{
throw new IOException(SR.Format(SR.UnauthorizedAccess_IODenied_Path, fullPath));
}
throw Interop.GetExceptionForIoErrno(errorInfo, fullPath, isDirError: true);
}
return true;
}
/// <summary>Determines whether the specified directory name should be ignored.</summary>
/// <param name="name">The name to evaluate.</param>
/// <returns>true if the name is "." or ".."; otherwise, false.</returns>
private static bool ShouldIgnoreDirectory(string name)
{
return name == "." || name == "..";
}
public static FileAttributes GetAttributes(string fullPath)
{
FileAttributes attributes = new FileInfo(fullPath, null).Attributes;
if (attributes == (FileAttributes)(-1))
throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(Interop.Error.ENOENT), fullPath);
return attributes;
}
public static FileAttributes GetAttributes(SafeFileHandle fileHandle)
=> default(FileStatus).GetAttributes(fileHandle);
public static void SetAttributes(string fullPath, FileAttributes attributes)
=> default(FileStatus).SetAttributes(fullPath, attributes, asDirectory: false);
public static void SetAttributes(SafeFileHandle fileHandle, FileAttributes attributes)
=> default(FileStatus).SetAttributes(fileHandle, attributes, asDirectory: false);
public static UnixFileMode GetUnixFileMode(string fullPath)
{
UnixFileMode mode = default(FileStatus).GetUnixFileMode(fullPath);
if (mode == (UnixFileMode)(-1))
throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(Interop.Error.ENOENT), fullPath);
return mode;
}
public static UnixFileMode GetUnixFileMode(SafeFileHandle fileHandle)
=> default(FileStatus).GetUnixFileMode(fileHandle);
public static void SetUnixFileMode(string fullPath, UnixFileMode mode)
=> default(FileStatus).SetUnixFileMode(fullPath, mode);
public static void SetUnixFileMode(SafeFileHandle fileHandle, UnixFileMode mode)
=> default(FileStatus).SetUnixFileMode(fileHandle, mode);
public static DateTimeOffset GetCreationTime(string fullPath)
=> default(FileStatus).GetCreationTime(fullPath).UtcDateTime;
public static DateTimeOffset GetCreationTime(SafeFileHandle fileHandle)
=> default(FileStatus).GetCreationTime(fileHandle).UtcDateTime;
public static void SetCreationTime(string fullPath, DateTimeOffset time, bool asDirectory)
=> default(FileStatus).SetCreationTime(fullPath, time, asDirectory);
public static void SetCreationTime(SafeFileHandle fileHandle, DateTimeOffset time)
=> default(FileStatus).SetCreationTime(fileHandle, time, asDirectory: false);
public static DateTimeOffset GetLastAccessTime(string fullPath)
=> default(FileStatus).GetLastAccessTime(fullPath).UtcDateTime;
public static DateTimeOffset GetLastAccessTime(SafeFileHandle fileHandle)
=> default(FileStatus).GetLastAccessTime(fileHandle).UtcDateTime;
public static void SetLastAccessTime(string fullPath, DateTimeOffset time, bool asDirectory)
=> default(FileStatus).SetLastAccessTime(fullPath, time, asDirectory);
public static unsafe void SetLastAccessTime(SafeFileHandle fileHandle, DateTimeOffset time)
=> default(FileStatus).SetLastAccessTime(fileHandle, time, asDirectory: false);
public static DateTimeOffset GetLastWriteTime(string fullPath)
=> default(FileStatus).GetLastWriteTime(fullPath).UtcDateTime;
public static DateTimeOffset GetLastWriteTime(SafeFileHandle fileHandle)
=> default(FileStatus).GetLastWriteTime(fileHandle).UtcDateTime;
public static void SetLastWriteTime(string fullPath, DateTimeOffset time, bool asDirectory)
=> default(FileStatus).SetLastWriteTime(fullPath, time, asDirectory);
public static unsafe void SetLastWriteTime(SafeFileHandle fileHandle, DateTimeOffset time)
=> default(FileStatus).SetLastWriteTime(fileHandle, time, asDirectory: false);
public static string[] GetLogicalDrives()
{
return DriveInfoInternal.GetLogicalDrives();
}
#pragma warning disable IDE0060
internal static string? GetLinkTarget(ReadOnlySpan<char> linkPath, bool isDirectory) => Interop.Sys.ReadLink(linkPath);
internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory)
{
Interop.CheckIo(Interop.Sys.SymLink(pathToTarget, path), path);
}
#pragma warning restore IDE0060
internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory)
{
ValueStringBuilder sb = new(Interop.DefaultPathBufferSize);
sb.Append(linkPath);
string? linkTarget = Interop.Sys.ReadLink(linkPath);
if (linkTarget == null)
{
sb.Dispose();
Interop.Error error = Interop.Sys.GetLastError();
// Not a link, return null
if (error == Interop.Error.EINVAL)
{
return null;
}
throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(error), linkPath, isDirectory);
}
if (!returnFinalTarget)
{
GetLinkTargetFullPath(ref sb, linkTarget);
}
else
{
string? current = linkTarget;
int visitCount = 1;
while (current != null)
{
if (visitCount > MaxFollowedLinks)
{
sb.Dispose();
// We went over the limit and couldn't reach the final target
throw new IOException(SR.Format(SR.IO_TooManySymbolicLinkLevels, linkPath));
}
GetLinkTargetFullPath(ref sb, current);
current = Interop.Sys.ReadLink(sb.AsSpan());
visitCount++;
}
}
Debug.Assert(sb.Length > 0);
linkTarget = sb.ToString(); // ToString disposes
return isDirectory ?
new DirectoryInfo(linkTarget) :
new FileInfo(linkTarget);
// In case of link target being relative:
// Preserve the full path of the directory of the previous path
// so the final target is returned with a valid full path
static void GetLinkTargetFullPath(ref ValueStringBuilder sb, ReadOnlySpan<char> linkTarget)
{
if (PathInternal.IsPartiallyQualified(linkTarget))
{
sb.Length = Path.GetDirectoryNameOffset(sb.AsSpan());
sb.Append(PathInternal.DirectorySeparatorChar);
}
else
{
sb.Length = 0;
}
sb.Append(linkTarget);
}
}
}
}
|