File: FileSystemInfoExtensions.cs
Web Access
Project: src\src\sdk\src\TemplateEngine\Microsoft.TemplateEngine.Utils\Microsoft.TemplateEngine.Utils.csproj (Microsoft.TemplateEngine.Utils)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.TemplateEngine.Abstractions.Mount;

namespace Microsoft.TemplateEngine.Utils
{
    public static class FileSystemInfoExtensions
    {
        public static void CopyTo(this IDirectory source, string target)
        {
            source.MountPoint.EnvironmentSettings.Host.FileSystem.CreateDirectory(target);

            foreach (IFile file in source.EnumerateFiles("*", SearchOption.TopDirectoryOnly))
            {
                using Stream f = source.MountPoint.EnvironmentSettings.Host.FileSystem.CreateFile(Path.Combine(target, file.Name));
                using Stream s = file.OpenRead();
                s.CopyTo(f);
                f.Flush();
            }

            foreach (IDirectory dir in source.EnumerateDirectories("*", SearchOption.TopDirectoryOnly))
            {
                dir.CopyTo(Path.Combine(target, dir.Name));
            }
        }

        public static string NormalizePath(this string path)
        {
            return path.Replace('\\', '/');
        }

        public static string CombinePaths(this string basePath, params string[] paths)
        {
            Stack<string> partStack = new Stack<string>();

            ProcessPath(basePath, partStack);

            for (int i = 0; i < paths.Length; ++i)
            {
                ProcessPath(paths[i], partStack);
            }

            return "/" + string.Join("/", partStack.Reverse());
        }

        public static IFile? FileInfo(this IFileSystemInfo info, string path)
        {
            string fullPath = info.FullPath.CombinePaths(path);
            return info.MountPoint.FileInfo(fullPath);
        }

        public static IDirectory? DirectoryInfo(this IFileSystemInfo info, string path)
        {
            string fullPath = info.FullPath.CombinePaths(path);
            return info.MountPoint.DirectoryInfo(fullPath);
        }

        public static IFileSystemInfo? FileSystemInfo(this IFileSystemInfo info, string path)
        {
            string fullPath = info.FullPath.CombinePaths(path);
            return info.MountPoint.FileSystemInfo(fullPath);
        }

        public static string PathRelativeTo(this IFileSystemInfo info, IFileSystemInfo relativeTo)
        {
            //The path should be relative to either source itself (in the case that it's a folder) or the parent of source)
            IDirectory? relTo = relativeTo as IDirectory ?? relativeTo.Parent;

            //If the thing to be relative to is the root (or a file in the root), just use the full path of the item
            if (relTo == null)
            {
                return info.FullPath;
            }

            //Get all the path segments for the thing we're relative to
            Dictionary<string, int> sourceSegments = new Dictionary<string, int> { { relTo.FullPath, 0 } };
            IDirectory? current = relTo.Parent;
            int index = 0;
            while (current != null)
            {
                sourceSegments[current.FullPath] = ++index;
                current = current.Parent;
            }

            current = info.Parent;
            List<string> segments = new List<string> { info.Name };

            //Walk back the set of parents of this item until one is contained by our source, building up a list as we go

#pragma warning disable IDE0018 // Inline variable declaration
            //If inlined, this breaks compilation for use of an unassigned variable
            int revIndex = 0;
#pragma warning restore IDE0018 // Inline variable declaration
            while (current != null && !sourceSegments.TryGetValue(current.FullPath, out revIndex))
            {
                segments.Insert(0, current.Name);
                current = current.Parent;
            }

            //Now that we've found our common point (and the index of the common segment _from the end_ of the source's parent chain)
            //  the number of levels up we need to go is the value of revIndex
            segments.InsertRange(0, Enumerable.Repeat("..", revIndex));
            return string.Join("/", segments);
        }

        private static void ProcessPath(string path, Stack<string> partStack)
        {
            if (string.IsNullOrEmpty(path))
            {
                return;
            }

            string[] parts = path.Split('/');

            for (int i = 0; i < parts.Length; ++i)
            {
                switch (parts[i])
                {
                    case "":
                        if (i == 0)
                        {
                            partStack.Clear();
                        }
                        break;
                    case ".":
                        break;
                    case "..":
                        if (partStack.Count == 0)
                        {
                            throw new IOException($"Failed to combine paths, stack underflow at {string.Join("/", parts.Skip(i))}");
                        }

                        _ = partStack.Pop();
                        break;
                    default:
                        partStack.Push(parts[i]);
                        break;
                }
            }
        }
    }
}