File: src\Compilers\Core\Portable\FileSystem\FileUtilities.cs
Web Access
Project: src\src\Interactive\Host\Microsoft.CodeAnalysis.InteractiveHost.csproj (Microsoft.CodeAnalysis.InteractiveHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
 
namespace Roslyn.Utilities
{
    internal static class FileUtilities
    {
        /// <summary>
        /// Resolves relative path and returns absolute path.
        /// The method depends only on values of its parameters and their implementation (for fileExists).
        /// It doesn't itself depend on the state of the current process (namely on the current drive directories) or 
        /// the state of file system.
        /// </summary>
        /// <param name="path">
        /// Path to resolve.
        /// </param>
        /// <param name="basePath">
        /// Base file path to resolve CWD-relative paths against. Null if not available.
        /// </param>
        /// <param name="baseDirectory">
        /// Base directory to resolve CWD-relative paths against if <paramref name="basePath"/> isn't specified. 
        /// Must be absolute path.
        /// Null if not available.
        /// </param>
        /// <param name="searchPaths">
        /// Sequence of paths used to search for unqualified relative paths.
        /// </param>
        /// <param name="fileExists">
        /// Method that tests existence of a file.
        /// </param>
        /// <returns>
        /// The resolved path or null if the path can't be resolved or does not exist.
        /// </returns>
        internal static string? ResolveRelativePath(
            string path,
            string? basePath,
            string? baseDirectory,
            IEnumerable<string> searchPaths,
            Func<string, bool> fileExists)
        {
            Debug.Assert(baseDirectory == null || searchPaths != null || PathUtilities.IsAbsolute(baseDirectory));
            RoslynDebug.Assert(searchPaths != null);
            RoslynDebug.Assert(fileExists != null);
 
            string? combinedPath;
            var kind = PathUtilities.GetPathKind(path);
            if (kind == PathKind.Relative)
            {
                // first, look in the base directory:
                baseDirectory = GetBaseDirectory(basePath, baseDirectory);
                if (baseDirectory != null)
                {
                    combinedPath = PathUtilities.CombinePathsUnchecked(baseDirectory, path);
                    Debug.Assert(PathUtilities.IsAbsolute(combinedPath));
                    if (fileExists(combinedPath))
                    {
                        return combinedPath;
                    }
                }
 
                // try search paths:
                foreach (var searchPath in searchPaths)
                {
                    combinedPath = PathUtilities.CombinePathsUnchecked(searchPath, path);
                    Debug.Assert(PathUtilities.IsAbsolute(combinedPath));
                    if (fileExists(combinedPath))
                    {
                        return combinedPath;
                    }
                }
 
                return null;
            }
 
            combinedPath = ResolveRelativePath(kind, path, basePath, baseDirectory);
            if (combinedPath != null)
            {
                Debug.Assert(PathUtilities.IsAbsolute(combinedPath));
                if (fileExists(combinedPath))
                {
                    return combinedPath;
                }
            }
 
            return null;
        }
 
        internal static string? ResolveRelativePath(string? path, string? baseDirectory)
        {
            return ResolveRelativePath(path, null, baseDirectory);
        }
 
        internal static string? ResolveRelativePath(string? path, string? basePath, string? baseDirectory)
        {
            Debug.Assert(baseDirectory == null || PathUtilities.IsAbsolute(baseDirectory));
            return ResolveRelativePath(PathUtilities.GetPathKind(path), path, basePath, baseDirectory);
        }
 
        private static string? ResolveRelativePath(PathKind kind, string? path, string? basePath, string? baseDirectory)
        {
            Debug.Assert(PathUtilities.GetPathKind(path) == kind);
 
            switch (kind)
            {
                case PathKind.Empty:
                    return null;
 
                case PathKind.Relative:
                    baseDirectory = GetBaseDirectory(basePath, baseDirectory);
                    if (baseDirectory == null)
                    {
                        return null;
                    }
 
                    // with no search paths relative paths are relative to the base directory:
                    return PathUtilities.CombinePathsUnchecked(baseDirectory, path);
 
                case PathKind.RelativeToCurrentDirectory:
                    baseDirectory = GetBaseDirectory(basePath, baseDirectory);
                    if (baseDirectory == null)
                    {
                        return null;
                    }
 
                    if (path!.Length == 1)
                    {
                        // "."
                        return baseDirectory;
                    }
                    else
                    {
                        // ".\path"
                        return PathUtilities.CombinePathsUnchecked(baseDirectory, path);
                    }
 
                case PathKind.RelativeToCurrentParent:
                    baseDirectory = GetBaseDirectory(basePath, baseDirectory);
                    if (baseDirectory == null)
                    {
                        return null;
                    }
 
                    // ".."
                    return PathUtilities.CombinePathsUnchecked(baseDirectory, path);
 
                case PathKind.RelativeToCurrentRoot:
                    string? baseRoot;
                    if (basePath != null)
                    {
                        baseRoot = PathUtilities.GetPathRoot(basePath);
                    }
                    else if (baseDirectory != null)
                    {
                        baseRoot = PathUtilities.GetPathRoot(baseDirectory);
                    }
                    else
                    {
                        return null;
                    }
 
                    if (RoslynString.IsNullOrEmpty(baseRoot))
                    {
                        return null;
                    }
 
                    Debug.Assert(PathUtilities.IsDirectorySeparator(path![0]));
                    Debug.Assert(path.Length == 1 || !PathUtilities.IsDirectorySeparator(path[1]));
                    return PathUtilities.CombinePathsUnchecked(baseRoot, path.Substring(1));
 
                case PathKind.RelativeToDriveDirectory:
                    // drive relative paths not supported, can't resolve:
                    return null;
 
                case PathKind.Absolute:
                    return path;
 
                default:
                    throw ExceptionUtilities.UnexpectedValue(kind);
            }
        }
 
        private static string? GetBaseDirectory(string? basePath, string? baseDirectory)
        {
            // relative base paths are relative to the base directory:
            string? resolvedBasePath = ResolveRelativePath(basePath, baseDirectory);
            if (resolvedBasePath == null)
            {
                return baseDirectory;
            }
 
            // Note: Path.GetDirectoryName doesn't normalize the path and so it doesn't depend on the process state.
            Debug.Assert(PathUtilities.IsAbsolute(resolvedBasePath));
            try
            {
                return Path.GetDirectoryName(resolvedBasePath);
            }
            catch (Exception)
            {
                return null;
            }
        }
 
        private static readonly char[] s_invalidPathChars = Path.GetInvalidPathChars();
 
        internal static string GetNormalizedPathOrOriginalPath(string path, string? basePath)
        {
            return NormalizeRelativePath(path, basePath, baseDirectory: null) ?? path;
        }
 
        internal static string? NormalizeRelativePath(string path, string? basePath, string? baseDirectory)
        {
            // Does this look like a URI at all or does it have any invalid path characters? If so, just use it as is.
            if (path.IndexOf("://", StringComparison.Ordinal) >= 0 || path.IndexOfAny(s_invalidPathChars) >= 0)
            {
                return null;
            }
 
            string? resolvedPath = ResolveRelativePath(path, basePath, baseDirectory);
            if (resolvedPath == null)
            {
                return null;
            }
 
            string? normalizedPath = TryNormalizeAbsolutePath(resolvedPath);
            if (normalizedPath == null)
            {
                return null;
            }
 
            return normalizedPath;
        }
 
        /// <summary>
        /// Normalizes an absolute path.
        /// </summary>
        /// <param name="path">Path to normalize.</param>
        /// <exception cref="IOException"/>
        /// <returns>Normalized path.</returns>
        internal static string NormalizeAbsolutePath(string path)
        {
            // we can only call GetFullPath on an absolute path to avoid dependency on process state (current directory):
            Debug.Assert(PathUtilities.IsAbsolute(path));
 
            try
            {
                return Path.GetFullPath(path);
            }
            catch (ArgumentException e)
            {
                throw new IOException(e.Message, e);
            }
            catch (System.Security.SecurityException e)
            {
                throw new IOException(e.Message, e);
            }
            catch (NotSupportedException e)
            {
                throw new IOException(e.Message, e);
            }
        }
 
        internal static string NormalizeDirectoryPath(string path)
        {
            return NormalizeAbsolutePath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
        }
 
        internal static string? TryNormalizeAbsolutePath(string path)
        {
            if (!PathUtilities.IsAbsolute(path))
            {
                return null;
            }
 
            try
            {
                return Path.GetFullPath(path);
            }
            catch
            {
                return null;
            }
        }
 
        internal static Stream OpenRead(string fullPath)
        {
            Debug.Assert(PathUtilities.IsAbsolute(fullPath));
 
            try
            {
                return new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            }
            catch (IOException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new IOException(e.Message, e);
            }
        }
 
        internal static Stream OpenAsyncRead(string fullPath)
        {
            Debug.Assert(PathUtilities.IsAbsolute(fullPath));
 
            return RethrowExceptionsAsIOException(() => new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous));
        }
 
        public static T RethrowExceptionsAsIOException<T>(Func<T> operation)
            => RethrowExceptionsAsIOException(
                static operation => operation(),
                operation);
 
        public static T RethrowExceptionsAsIOException<T, TArg>(Func<TArg, T> operation, TArg arg)
        {
            try
            {
                return operation(arg);
            }
            catch (Exception e) when (e is not IOException)
            {
                throw new IOException(e.Message, e);
            }
        }
 
        public static Task<T> RethrowExceptionsAsIOExceptionAsync<T>(Func<Task<T>> operation)
            => RethrowExceptionsAsIOExceptionAsync(
                static operation => operation(),
                operation);
 
        public static async Task<T> RethrowExceptionsAsIOExceptionAsync<T, TArg>(Func<TArg, Task<T>> operation, TArg arg)
        {
            try
            {
                return await operation(arg).ConfigureAwait(false);
            }
            catch (Exception e) when (e is not IOException)
            {
                throw new IOException(e.Message, e);
            }
        }
 
        /// <summary>
        /// Used to create a file given a path specified by the user.
        /// paramName - Provided by the Public surface APIs to have a clearer message. Internal API just rethrow the exception
        /// </summary>
        internal static Stream CreateFileStreamChecked(Func<string, Stream> factory, string path, string? paramName = null)
        {
            try
            {
                return factory(path);
            }
            catch (ArgumentNullException)
            {
                if (paramName == null)
                {
                    throw;
                }
                else
                {
                    throw new ArgumentNullException(paramName);
                }
            }
            catch (ArgumentException e)
            {
                if (paramName == null)
                {
                    throw;
                }
                else
                {
                    throw new ArgumentException(e.Message, paramName);
                }
            }
            catch (IOException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new IOException(e.Message, e);
            }
        }
 
        /// <exception cref="IOException"/>
        internal static DateTime GetFileTimeStamp(string fullPath)
        {
            Debug.Assert(PathUtilities.IsAbsolute(fullPath));
            try
            {
                return File.GetLastWriteTimeUtc(fullPath);
            }
            catch (IOException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new IOException(e.Message, e);
            }
        }
 
        /// <exception cref="IOException"/>
        internal static long GetFileLength(string fullPath)
        {
            Debug.Assert(PathUtilities.IsAbsolute(fullPath));
            try
            {
                var info = new FileInfo(fullPath);
                return info.Length;
            }
            catch (IOException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new IOException(e.Message, e);
            }
        }
 
        /// <exception cref="IOException"/>
        /// <summary>
        /// Preferred mechanism to obtain both length and last write time of a file. Querying independently
        /// requires multiple i/o hits which are expensive, even if cached.
        /// </summary>
        internal static void GetFileLengthAndTimeStamp(string fullPath, out long fileLength, out DateTime timeStamp)
        {
            Debug.Assert(PathUtilities.IsAbsolute(fullPath));
            try
            {
                var info = new FileInfo(fullPath);
 
                fileLength = info.Length;
                timeStamp = info.LastWriteTimeUtc;
            }
            catch (IOException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new IOException(e.Message, e);
            }
        }
    }
}