// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Management.Automation.Provider;

using Dbg = System.Management.Automation;

#pragma warning disable 1634, 1691 // Stops compiler from warning about unknown warnings
#pragma warning disable 56500

namespace System.Management.Automation
{
    /// <summary>
    /// Holds the state of a Monad Shell session.
    /// </summary>
    internal sealed partial class SessionStateInternal
    {
        /// <summary>
        /// The currently active drive. It determines the current working directory.
        /// </summary>
        private PSDriveInfo _currentDrive;

        #region NewDrive

        /// <summary>
        /// Adds the specified drive to the current scope.
        /// </summary>
        /// <param name="drive">
        /// The drive to be added to the current scope.
        /// </param>
        /// <param name="scopeID">
        /// The ID for the scope to add the drive to. The scope ID can be any of the
        /// "special" scope identifiers like "global", "local", or "private" or it
        /// can be a numeric identifier that is a count of the number of parent
        /// scopes up from the current scope to put the drive in.
        /// If this parameter is null or empty the drive will be placed in the
        /// current scope.
        /// </param>
        /// <returns>
        /// The drive that was added, if any.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="drive"/> is null.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// If the drive already exists,
        /// or
        /// If <paramref name="drive"/>.Name contains one or more invalid characters; ~ / \\ . :
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// If the provider is not a DriveCmdletProvider.
        /// </exception>
        /// <exception cref="ProviderNotFoundException">
        /// The provider for the <paramref name="drive"/> could not be found.
        /// </exception>
        /// <exception cref="ProviderInvocationException">
        /// If the provider threw an exception or returned null.
        /// </exception>
        internal PSDriveInfo NewDrive(PSDriveInfo drive, string scopeID)
        {
            if (drive == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(drive));
            }

            PSDriveInfo result = null;

            // Construct a CmdletProviderContext and call the override

            CmdletProviderContext context = new CmdletProviderContext(this.ExecutionContext);

            NewDrive(drive, scopeID, context);

            context.ThrowFirstErrorOrDoNothing();

            Collection<PSObject> successObjects = context.GetAccumulatedObjects();

            if (successObjects != null &&
                successObjects.Count > 0)
            {
                Dbg.Diagnostics.Assert(
                    successObjects.Count == 1,
                    "NewDrive should only add one PSDriveInfo object to the pipeline");

                // set the return value to the first drive (should only be one).

                if (!successObjects[0].ImmediateBaseObjectIsEmpty)
                {
                    result = (PSDriveInfo)successObjects[0].BaseObject;
                }
            }

            return result;
        }

        /// <summary>
        /// Adds a drive to the PowerShell namespace.
        /// </summary>
        /// <param name="drive">
        /// The new drive to be added.
        /// </param>
        /// <param name="scopeID">
        /// The ID for the scope to add the drive to. The scope ID can be any of the
        /// "special" scope identifiers like "global", "local", or "private" or it
        /// can be a numeric identifier that is a count of the number of parent
        /// scopes up from the current scope to put the drive in.
        /// If this parameter is null or empty the drive will be placed in the
        /// current scope.
        /// </param>
        /// <param name="context">
        /// The context which the core command is running.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="drive"/> or <paramref name="context"/> is null.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// If the drive already exists
        /// or
        /// If <paramref name="drive"/>.Name contains one or more invalid characters; ~ / \\ . :
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// If <paramref name="scopeID"/> is less than zero or greater than the number of currently
        /// active scopes.
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// If the provider is not a DriveCmdletProvider.
        /// </exception>
        /// <exception cref="ProviderNotFoundException">
        /// The provider for the <paramref name="drive"/> could not be found.
        /// </exception>
        /// <exception cref="ProviderInvocationException">
        /// If the provider threw an exception or returned null.
        /// </exception>
        internal void NewDrive(PSDriveInfo drive, string scopeID, CmdletProviderContext context)
        {
            if (drive == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(drive));
            }

            if (context == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(context));
            }

            if (!IsValidDriveName(drive.Name))
            {
                ArgumentException e =
                    PSTraceSource.NewArgumentException(
                        "drive.Name",
                        SessionStateStrings.DriveNameIllegalCharacters);

                throw e;
            }

            // Allow the provider a chance to approve the drive and set
            // provider specific data

            PSDriveInfo result = ValidateDriveWithProvider(drive, context, true);

            // We assume that the provider wrote the error message as they
            // are suppose to.
            if (result == null)
            {
                return;
            }

            if (string.Equals(result.Name, drive.Name, StringComparison.OrdinalIgnoreCase))
            {
                // Set the drive in the current scope.

                try
                {
                    SessionStateScope scope = _currentScope;

                    if (!string.IsNullOrEmpty(scopeID))
                    {
                        scope = GetScopeByID(scopeID);
                    }

                    scope.NewDrive(result);
                }
                catch (ArgumentException argumentException)
                {
                    // Wrap up the exception and write it to the error stream

                    context.WriteError(
                        new ErrorRecord(
                            argumentException,
                            "NewDriveError",
                            ErrorCategory.InvalidArgument,
                            result));
                    return;
                }
                catch (SessionStateException)
                {
                    // This should be a pipeline terminating condition
                    throw;
                }

                if (ProvidersCurrentWorkingDrive[drive.Provider] == null)
                {
                    // Set the new drive as the current
                    // drive for the provider since there isn't one set.

                    ProvidersCurrentWorkingDrive[drive.Provider] = drive;
                }

                // Upon success, write the drive to the pipeline

                context.WriteObject(result);
            }
            else
            {
                ProviderInvocationException e =
                    NewProviderInvocationException(
                        "NewDriveProviderFailed",
                        SessionStateStrings.NewDriveProviderFailed,
                        drive.Provider,
                        drive.Root,
                        PSTraceSource.NewArgumentException("root"));

                throw e;
            }
        }

        private static bool IsValidDriveName(string name)
        {
            const string CharactersInvalidInDriveName = ":/\\.~";

            return !string.IsNullOrEmpty(name)
                && name.AsSpan().IndexOfAny(CharactersInvalidInDriveName) < 0;
        }

        /// <summary>
        /// Tries to resolve the drive root as an MSH path. If it successfully resolves
        /// to a single path then the resolved provider internal path is returned. If it
        /// does not resolve to a single MSH path the root is returned as it was passed.
        /// </summary>
        /// <param name="root">
        /// The root path of the drive to be resolved.
        /// </param>
        /// <param name="provider">
        /// The provider that should be used when resolving the path.
        /// </param>
        /// <returns>
        /// The new root path of the drive.
        /// </returns>
        private string GetProviderRootFromSpecifiedRoot(string root, ProviderInfo provider)
        {
            Dbg.Diagnostics.Assert(
                root != null,
                "Caller should have verified the root");

            Dbg.Diagnostics.Assert(
                provider != null,
                "Caller should have verified the provider");

            string result = root;

            SessionState sessionState = new SessionState(ExecutionContext.TopLevelSessionState);
            Collection<string> resolvedPaths = null;
            ProviderInfo resolvedProvider = null;

            try
            {
                // First try to resolve the root as an MSH path

                resolvedPaths =
                    sessionState.Path.GetResolvedProviderPathFromPSPath(root, out resolvedProvider);

                // If a single path was resolved...

                if (resolvedPaths != null &&
                    resolvedPaths.Count == 1)
                {
                    // and the provider used to resolve the path,
                    // matches the one specified by the drive...

                    if (provider.NameEquals(resolvedProvider.FullName))
                    {
                        // and the item exists

                        ProviderIntrinsics providerIntrinsics =
                            new ProviderIntrinsics(this);

                        if (providerIntrinsics.Item.Exists(root))
                        {
                            // then use the resolved path as the root of the drive

                            result = resolvedPaths[0];
                        }
                    }
                }
            }
            catch (LoopFlowException)
            {
                throw;
            }
            catch (PipelineStoppedException)
            {
                throw;
            }
            catch (ActionPreferenceStopException)
            {
                throw;
            }
            // If any of the following exceptions are thrown we assume that
            // the path is a file system path not an MSH path and try
            // to create the drive with that root.
            catch (DriveNotFoundException)
            {
            }
            catch (ProviderNotFoundException)
            {
            }
            catch (ItemNotFoundException)
            {
            }
            catch (NotSupportedException)
            {
            }
            catch (InvalidOperationException)
            {
            }
            catch (ProviderInvocationException)
            {
            }
            catch (ArgumentException)
            {
            }

            return result;
        }

        /// <summary>
        /// Gets an object that defines the additional parameters for the NewDrive implementation
        /// for a provider.
        /// </summary>
        /// <param name="providerId">
        /// The provider ID for the drive that is being created.
        /// </param>
        /// <param name="context">
        /// The context under which this method is being called.
        /// </param>
        /// <returns>
        /// An object that has properties and fields decorated with
        /// parsing attributes similar to a cmdlet class.
        /// </returns>
        /// <exception cref="NotSupportedException">
        /// If the <paramref name="providerId"/> is not a DriveCmdletProvider.
        /// </exception>
        /// <exception cref="ProviderNotFoundException">
        /// If <paramref name="providerId"/> does not exist.
        /// </exception>
        internal object NewDriveDynamicParameters(string providerId, CmdletProviderContext context)
        {
            if (providerId == null)
            {
                // If the provider hasn't been specified yet, just return null.
                // The provider can be specified as pipeline input.
                return null;
            }

            DriveCmdletProvider provider = GetDriveProviderInstance(providerId);

            object result = null;
            try
            {
                result = provider.NewDriveDynamicParameters(context);
            }
            catch (Exception e) // Catch-all OK, 3rd party callout
            {
                throw
                    NewProviderInvocationException(
                        "NewDriveDynamicParametersProviderException",
                        SessionStateStrings.NewDriveDynamicParametersProviderException,
                        provider.ProviderInfo,
                        null,
                        e);
            }

            return result;
        }

        #endregion NewDrive

        #region GetDrive

        /// <summary>
        /// Searches through the session state scopes to find a drive.
        /// </summary>
        /// <param name="name">
        /// The name of a drive to find.
        /// </param>
        /// <returns>
        /// The drive information if the drive is found.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="name"/> is null.
        /// </exception>
        /// <exception cref="DriveNotFoundException">
        /// If there is no drive with <paramref name="name"/>.
        /// </exception>
        internal PSDriveInfo GetDrive(string name)
        {
            return GetDrive(name, true);
        }

        private PSDriveInfo GetDrive(string name, bool automount)
        {
            if (name == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(name));
            }

            PSDriveInfo result = null;

            // Start searching through the scopes for the drive until the drive
            // is found or the global scope is reached.

            SessionStateScopeEnumerator scopeEnumerator = new SessionStateScopeEnumerator(CurrentScope);

            int scopeID = 0;

            foreach (SessionStateScope processingScope in scopeEnumerator)
            {
                result = processingScope.GetDrive(name);

                if (result != null)
                {
                    if (result.IsAutoMounted)
                    {
                        // Validate or remove the auto-mounted drive

                        if (!ValidateOrRemoveAutoMountedDrive(result, processingScope))
                        {
                            result = null;
                        }
                    }

                    if (result != null)
                    {
                        s_tracer.WriteLine("Drive found in scope {0}", scopeID);
                        break;
                    }
                }

                // Increment the scope ID
                ++scopeID;
            }

            if (result == null && automount)
            {
                // Attempt to automount as a file system drive
                // or as a BuiltIn drive (e.g. "Cert"/"Certificate"/"WSMan")
                result = AutomountFileSystemDrive(name) ?? AutomountBuiltInDrive(name);
            }

            if (result == null)
            {
                DriveNotFoundException driveNotFound =
                    new DriveNotFoundException(
                        name,
                        "DriveNotFound",
                        SessionStateStrings.DriveNotFound);

                throw driveNotFound;
            }

            return result;
        }

        /// <summary>
        /// Searches through the session state scopes looking
        /// for a drive of the specified name.
        /// </summary>
        /// <param name="name">
        /// The name of the drive to return.
        /// </param>
        /// <param name="scopeID">
        /// The scope ID of the scope to look in for the drive.
        /// If this parameter is null or empty the drive will be
        /// found by searching the scopes using the dynamic scoping
        /// rules.
        /// </param>
        /// <returns>
        /// The drive for the given name in the given scope or null if
        /// the drive was not found.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="name"/> is null.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// If <paramref name="scopeID"/> is less than zero, or not
        /// a number and not "script", "global", "local", or "private"
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// If <paramref name="scopeID"/> is less than zero or greater than the number of currently
        /// active scopes.
        /// </exception>
        internal PSDriveInfo GetDrive(string name, string scopeID)
        {
            if (name == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(name));
            }

            PSDriveInfo result = null;

            // The scope ID wasn't defined or wasn't recognizable
            // so do a search through the scopes looking for the
            // drive.

            if (string.IsNullOrEmpty(scopeID))
            {
                SessionStateScopeEnumerator scopeEnumerator =
                    new SessionStateScopeEnumerator(CurrentScope);

                foreach (SessionStateScope scope in scopeEnumerator)
                {
                    result = scope.GetDrive(name);

                    if (result != null)
                    {
                        if (result.IsAutoMounted)
                        {
                            // Validate or remove the auto-mounted drive

                            if (!ValidateOrRemoveAutoMountedDrive(result, scope))
                            {
                                result = null;
                            }
                        }

                        if (result != null)
                        {
                            break;
                        }
                    }
                }

                if (result == null)
                {
                    result = AutomountFileSystemDrive(name);
                }
            }
            else
            {
                SessionStateScope scope = GetScopeByID(scopeID);
                result = scope.GetDrive(name);

                if (result != null)
                {
                    if (result.IsAutoMounted)
                    {
                        // Validate or remove the auto-mounted drive

                        if (!ValidateOrRemoveAutoMountedDrive(result, scope))
                        {
                            result = null;
                        }
                    }
                }
                else
                {
                    if (scope == GlobalScope)
                    {
                        result = AutomountFileSystemDrive(name);
                    }
                }
            }

            return result;
        }

        private PSDriveInfo AutomountFileSystemDrive(string name)
        {
            PSDriveInfo result = null;

            // Check to see if it could be a "auto-mounted"
            // file system drive.  If so, add the new drive
            // to the global scope and return it

            if (name.Length == 1)
            {
                try
                {
                    System.IO.DriveInfo driveInfo = new System.IO.DriveInfo(name);
                    result = AutomountFileSystemDrive(driveInfo);
                }
                catch (LoopFlowException)
                {
                    throw;
                }
                catch (PipelineStoppedException)
                {
                    throw;
                }
                catch (ActionPreferenceStopException)
                {
                    throw;
                }
                catch (Exception)
                {
                    // Catch all exceptions and continue since the drive does not exist
                    // This action wasn't requested by the user and as such we don't
                    // want to expose the user to any error conditions other than
                    // DriveNotFoundException which will be thrown by the caller
                }
            }

            return result;
        }

        private PSDriveInfo AutomountFileSystemDrive(System.IO.DriveInfo systemDriveInfo)
        {
            PSDriveInfo result = null;

            if (!IsProviderLoaded(this.ExecutionContext.ProviderNames.FileSystem))
            {
                s_tracer.WriteLine("The {0} provider is not loaded", this.ExecutionContext.ProviderNames.FileSystem);
                return null;
            }

            // Since the drive does exist, add it.

            try
            {
                // Get the FS provider

                DriveCmdletProvider driveProvider =
                    GetDriveProviderInstance(this.ExecutionContext.ProviderNames.FileSystem);

                if (driveProvider != null)
                {
                    // Create a new drive
                    string systemDriveName = systemDriveInfo.Name.Substring(0, 1);
                    string volumeLabel = string.Empty;
                    string displayRoot = null;

                    try
                    {
                        // When run in an AppContainer, we may not have access to the volume label.
                        volumeLabel = systemDriveInfo.VolumeLabel;
                    }
                    catch (UnauthorizedAccessException) { }

                    // Get the actual root path for Network type drives
                    if (systemDriveInfo.DriveType == DriveType.Network)
                    {
                        try
                        {
                            displayRoot = Microsoft.PowerShell.Commands.FileSystemProvider
                                            .GetRootPathForNetworkDriveOrDosDevice(systemDriveInfo);
                        }
                        // We want to get root path of the network drive as extra information to display to the user.
                        // It's okay we failed to get the root path for some reason. We don't want to throw exception
                        // here as it would break the current behavior.
                        catch (Win32Exception) { }
                        catch (InvalidOperationException) { }
                    }

                    PSDriveInfo newPSDriveInfo =
                        new PSDriveInfo(
                            systemDriveName,
                            driveProvider.ProviderInfo,
                            systemDriveInfo.RootDirectory.FullName,
                            volumeLabel,
                            null,
                            displayRoot);

                    newPSDriveInfo.IsAutoMounted = true;

                    CmdletProviderContext context = new CmdletProviderContext(this.ExecutionContext);

                    newPSDriveInfo.DriveBeingCreated = true;

                    // Validate the drive with the provider
                    result = ValidateDriveWithProvider(driveProvider, newPSDriveInfo, context, false);

                    newPSDriveInfo.DriveBeingCreated = false;

                    if (result != null && !context.HasErrors())
                    {
                        // Create the drive in the global scope.
                        GlobalScope.NewDrive(result);
                    }
                }
            }
            catch (LoopFlowException)
            {
                throw;
            }
            catch (PipelineStoppedException)
            {
                throw;
            }
            catch (ActionPreferenceStopException)
            {
                throw;
            }
            catch (Exception e)
            {
                // Since the user isn't expecting this behavior, we don't
                // want to let errors find their way out. If there are any
                // failures we just don't mount the drive.

                MshLog.LogProviderHealthEvent(
                    this.ExecutionContext,
                    this.ExecutionContext.ProviderNames.FileSystem,
                    e,
                    Severity.Warning);
            }

            return result;
        }

        /// <summary>
        /// Auto-mounts a built-in drive.
        /// </summary>
        /// <remarks>
        /// Calls GetDrive(name, false) internally.
        /// </remarks>
        /// <param name="name">The name of the drive to load.</param>
        /// <returns></returns>
        internal PSDriveInfo AutomountBuiltInDrive(string name)
        {
            MountDefaultDrive(name, ExecutionContext);
            PSDriveInfo result = GetDrive(name, false);

            return result;
        }

        /// <summary>
        /// Automatically mount the specified drive.
        /// </summary>
        /// <remarks>
        /// Neither 'WSMan' nor 'Certificate' provider works in UNIX PS today.
        /// So this method currently does nothing on UNIX.
        /// </remarks>
        internal static void MountDefaultDrive(string name, ExecutionContext context)
        {
#if !UNIX
            PSModuleAutoLoadingPreference moduleAutoLoadingPreference =
                CommandDiscovery.GetCommandDiscoveryPreference(context, SpecialVariables.PSModuleAutoLoadingPreferenceVarPath, "PSModuleAutoLoadingPreference");
            if (moduleAutoLoadingPreference == PSModuleAutoLoadingPreference.None)
            {
                return;
            }

            string moduleName = null;

            // Note: For the certificate provider, we actually support the provider name as an alternative to
            // mount the default drive, since the provider names can be used for provider-qualified paths.
            // The WSMAN drive is the same as the provider name.
            if (
                string.Equals("Cert", name, StringComparison.OrdinalIgnoreCase) ||
                string.Equals("Certificate", name, StringComparison.OrdinalIgnoreCase)
                )
            {
                moduleName = "Microsoft.PowerShell.Security";
            }
            else if (string.Equals("WSMan", name, StringComparison.OrdinalIgnoreCase))
            {
                moduleName = "Microsoft.WSMan.Management";
            }

            if (!string.IsNullOrEmpty(moduleName))
            {
                s_tracer.WriteLine("Auto-mounting built-in drive: {0}", name);
                CommandInfo commandInfo = new CmdletInfo("Import-Module", typeof(Microsoft.PowerShell.Commands.ImportModuleCommand), null, null, context);
                Exception exception = null;
                s_tracer.WriteLine("Attempting to load module: {0}", moduleName);
                CommandDiscovery.AutoloadSpecifiedModule(moduleName, context, commandInfo.Visibility, out exception);
                if (exception != null)
                {
                    // Call-out to user code, catch-all OK
                }
            }
#endif
        }

        /// <summary>
        /// Determines if the specified automounted drive still exists. If not,
        /// the drive is removed.
        /// </summary>
        /// <param name="drive">
        /// The drive to validate or remove.
        /// </param>
        /// <param name="scope">
        /// The scope the drive is in.  This will be used to remove the drive
        /// if necessary.
        /// </param>
        /// <returns>
        /// True if the drive is still valid, false if the drive was removed.
        /// </returns>
        private bool ValidateOrRemoveAutoMountedDrive(PSDriveInfo drive, SessionStateScope scope)
        {
            bool result = true;
            try
            {
                System.IO.DriveInfo systemDriveInfo = new System.IO.DriveInfo(drive.Name);
                result = systemDriveInfo.DriveType != DriveType.NoRootDirectory;
            }
            catch (LoopFlowException)
            {
                throw;
            }
            catch (PipelineStoppedException)
            {
                throw;
            }
            catch (ActionPreferenceStopException)
            {
                throw;
            }
            catch (Exception)
            {
                // Assume any exception means the drive is no longer valid and needs
                // to be removed.

                result = false;
            }

            if (!result)
            {
                DriveCmdletProvider driveProvider = null;

                try
                {
                    driveProvider =
                        GetDriveProviderInstance(this.ExecutionContext.ProviderNames.FileSystem);
                }
                catch (NotSupportedException)
                {
                }
                catch (ProviderNotFoundException)
                {
                }

                if (driveProvider != null)
                {
                    CmdletProviderContext context = new CmdletProviderContext(this.ExecutionContext);

                    try
                    {
                        // Give the provider a chance to cleanup
                        driveProvider.RemoveDrive(drive, context);
                    }
                    // Ignore any exceptions the provider throws because we
                    // are doing this without an explicit request from the
                    // user. Since the provider can throw any exception
                    // we must catch all exceptions here.
                    catch (Exception)
                    {
                    }

                    scope.RemoveDrive(drive);
                }
            }

            return result;
        }

        /// <summary>
        /// If a VHD is mounted to a drive prior to the PowerShell session being launched,
        /// then such a drive has to be validated for its existence before performing
        /// any operations on that drive to make sure that the drive is not unmounted.
        /// </summary>
        /// <param name="drive"></param>
        /// <returns>Absence of mounted drive for FileSystem provider or False for other provider types.</returns>
        private bool IsAStaleVhdMountedDrive(PSDriveInfo drive)
        {
            bool result = false;

            // check that drive's provider type is FileSystem
            if ((drive.Provider != null) && (!drive.Provider.NameEquals(this.ExecutionContext.ProviderNames.FileSystem)))
            {
                return false;
            }

            // A VHD mounted drive gets detected with a DriveType of DriveType.Fixed
            // when the VHD is mounted, however if the drive is unmounted, such a
            // stale drive is no longer valid and gets detected with DriveType.NoRootDirectory.
            // We would hit this situation in the following scenario:
            //  1. Launch Powershell session 'A' and mount the VHD.
            //  2. Launch different powershell session 'B'.
            //  3. Unmount the VHD in session 'A'.
            // The drive pointing to VHD in session 'B' gets detected as DriveType.NoRootDirectory
            // after the VHD is removed in session 'A'.
            if (drive != null && !string.IsNullOrEmpty(drive.Name) && drive.Name.Length == 1)
            {
                try
                {
                    char driveChar = Convert.ToChar(drive.Name, CultureInfo.InvariantCulture);

                    if (char.ToUpperInvariant(driveChar) >= 'A' && char.ToUpperInvariant(driveChar) <= 'Z')
                    {
                        DriveInfo systemDriveInfo = new DriveInfo(drive.Name);

                        if (systemDriveInfo.DriveType == DriveType.NoRootDirectory)
                        {
                            if (!Directory.Exists(drive.Root))
                            {
                                result = true;
                            }
                        }
                    }
                }
                catch (ArgumentException)
                {
                    // At this point, We dont care if the drive is not a valid drive that does not host the VHD.
                }
            }

            return result;
        }

        /// <summary>
        /// Gets all the drives for a specific provider.
        /// </summary>
        /// <param name="providerId">
        /// The identifier for the provider to retrieve the drives for.
        /// </param>
        /// <returns>
        /// An IEnumerable that contains the drives for the specified provider.
        /// </returns>
        internal Collection<PSDriveInfo> GetDrivesForProvider(string providerId)
        {
            if (string.IsNullOrEmpty(providerId))
            {
                return Drives(null);
            }

            // Ensure that the provider name resolves to a single provider
            GetSingleProvider(providerId);

            Collection<PSDriveInfo> drives = new Collection<PSDriveInfo>();

            foreach (PSDriveInfo drive in Drives(null))
            {
                if (drive != null &&
                    drive.Provider.NameEquals(providerId))
                {
                    drives.Add(drive);
                }
            }

            return drives;
        }

        #endregion GetDrive

        #region RemoveDrive
        /// <summary>
        /// Removes the drive with the specified name.
        /// </summary>
        /// <param name="driveName">
        /// The name of the drive to remove.
        /// </param>
        /// <param name="force">
        /// Determines whether drive should be forcefully removed even if there was errors.
        /// </param>
        /// <param name="scopeID">
        /// The ID of the scope from which to remove the drive.
        /// If the scope ID is null or empty, the scope hierarchy will be searched
        /// starting at the current scope through all the parent scopes to the
        /// global scope until a drive of the given name is found to remove.
        /// </param>
        internal void RemoveDrive(string driveName, bool force, string scopeID)
        {
            if (driveName == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(driveName));
            }

            PSDriveInfo drive = GetDrive(driveName, scopeID);

            if (drive == null)
            {
                DriveNotFoundException e = new DriveNotFoundException(
                    driveName,
                    "DriveNotFound",
                    SessionStateStrings.DriveNotFound);
                throw e;
            }

            RemoveDrive(drive, force, scopeID);
        }

        /// <summary>
        /// Removes the drive with the specified name.
        /// </summary>
        /// <param name="driveName">
        /// The name of the drive to remove.
        /// </param>
        /// <param name="force">
        /// Determines whether drive should be forcefully removed even if there was errors.
        /// </param>
        /// <param name="scopeID">
        /// The ID of the scope from which to remove the drive.
        /// If the scope ID is null or empty, the scope hierarchy will be searched
        /// starting at the current scope through all the parent scopes to the
        /// global scope until a drive of the given name is found to remove.
        /// </param>
        /// <param name="context">
        /// The context of the command.
        /// </param>
        internal void RemoveDrive(
            string driveName,
            bool force,
            string scopeID,
            CmdletProviderContext context)
        {
            if (driveName == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(driveName));
            }

            Dbg.Diagnostics.Assert(
                context != null,
                "The caller should verify the context");

            PSDriveInfo drive = GetDrive(driveName, scopeID);

            if (drive == null)
            {
                DriveNotFoundException e = new DriveNotFoundException(
                    driveName,
                    "DriveNotFound",
                    SessionStateStrings.DriveNotFound);
                context.WriteError(new ErrorRecord(e.ErrorRecord, e));
            }
            else
            {
                RemoveDrive(drive, force, scopeID, context);
            }
        }

        /// <summary>
        /// Removes the specified drive.
        /// </summary>
        /// <param name="drive">
        /// The drive to be removed.
        /// </param>
        /// <param name="force">
        /// Determines whether drive should be forcefully removed even if there was errors.
        /// </param>
        /// <param name="scopeID">
        /// The ID of the scope from which to remove the drive.
        /// If the scope ID is null or empty, the scope hierarchy will be searched
        /// starting at the current scope through all the parent scopes to the
        /// global scope until a drive of the given name is found to remove.
        /// </param>
        internal void RemoveDrive(PSDriveInfo drive, bool force, string scopeID)
        {
            if (drive == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(drive));
            }

            CmdletProviderContext context = new CmdletProviderContext(this.ExecutionContext);

            RemoveDrive(drive, force, scopeID, context);

            if (context.HasErrors() && !force)
            {
                context.ThrowFirstErrorOrDoNothing();
            }
        }

        /// <summary>
        /// Removes the specified drive.
        /// </summary>
        /// <param name="drive">
        /// The drive to be removed.
        /// </param>
        /// <param name="force">
        /// Determines whether drive should be forcefully removed even if there was errors.
        /// </param>
        /// <param name="scopeID">
        /// The ID of the scope from which to remove the drive.
        /// If the scope ID is null or empty, the scope hierarchy will be searched
        /// starting at the current scope through all the parent scopes to the
        /// global scope until a drive of the given name is found to remove.
        /// </param>
        /// <param name="context">
        /// The context which the core command is running.
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// If <paramref name="scopeID"/> is less than zero or greater than the number of currently
        /// active scopes.
        /// </exception>
        internal void RemoveDrive(
            PSDriveInfo drive,
            bool force,
            string scopeID,
            CmdletProviderContext context)
        {
            // Make sure that the CanRemoveDrive is called even if we are forcing
            // the removal because we want the provider to have a chance to
            // cleanup.

            bool canRemove = false;

            try
            {
                canRemove = CanRemoveDrive(drive, context);
            }
            catch (LoopFlowException)
            {
                throw;
            }
            catch (PipelineStoppedException)
            {
                throw;
            }
            catch (ActionPreferenceStopException)
            {
                throw;
            }
            catch (ProviderInvocationException)
            {
                if (!force)
                {
                    throw;
                }
            }

            // Now remove the drive if there was no error or we are forcing the removal

            if (canRemove || force)
            {
                // The scope ID wasn't defined or wasn't recognizable
                // so do a search through the scopes looking for the
                // drive.

                if (string.IsNullOrEmpty(scopeID))
                {
                    SessionStateScopeEnumerator scopeEnumerator =
                        new SessionStateScopeEnumerator(CurrentScope);

                    foreach (SessionStateScope scope in scopeEnumerator)
                    {
                        try
                        {
                            PSDriveInfo result = scope.GetDrive(drive.Name);
                            if (result != null)
                            {
                                scope.RemoveDrive(drive);

                                // If the drive is the current drive for the provider, remove
                                // it from the current drive list.

                                if (ProvidersCurrentWorkingDrive[drive.Provider] == result)
                                {
                                    ProvidersCurrentWorkingDrive[drive.Provider] = null;
                                }

                                break;
                            }
                        }
                        catch (ArgumentException)
                        {
                        }
                    }
                }
                else
                {
                    SessionStateScope scope = GetScopeByID(scopeID);
                    scope.RemoveDrive(drive);

                    // If the drive is the current drive for the provider, remove
                    // it from the current drive list.

                    if (ProvidersCurrentWorkingDrive[drive.Provider] == drive)
                    {
                        ProvidersCurrentWorkingDrive[drive.Provider] = null;
                    }
                }
            }
            else
            {
                PSInvalidOperationException e =
                    (PSInvalidOperationException)PSTraceSource.NewInvalidOperationException(
                        SessionStateStrings.DriveRemovalPreventedByProvider,
                        drive.Name,
                        drive.Provider);

                context.WriteError(
                    new ErrorRecord(
                        e.ErrorRecord,
                        e));
            }
        }

        /// <summary>
        /// Determines if the drive can be removed by calling the provider
        /// for the drive.
        /// </summary>
        /// <param name="drive">
        /// The drive to test for removal.
        /// </param>
        /// <param name="context">
        /// The context under which the command is running.
        /// </param>
        /// <returns>
        /// True if the drive can be removed, false otherwise.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// If <paramref name="drive"/> or <paramref name="context"/> is null.
        /// </exception>
        /// <exception cref="ProviderInvocationException">
        /// If the provider threw an exception when RemoveDrive was called.
        /// </exception>
        private bool CanRemoveDrive(PSDriveInfo drive, CmdletProviderContext context)
        {
            if (context == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(context));
            }

            if (drive == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(drive));
            }

            s_tracer.WriteLine("Drive name = {0}", drive.Name);

            // First set the drive data

            context.Drive = drive;

            // Now see if the provider will let us remove the drive

            DriveCmdletProvider driveCmdletProvider =
                GetDriveProviderInstance(drive.Provider);

            bool driveRemovable = false;

            PSDriveInfo result = null;

            try
            {
                result = driveCmdletProvider.RemoveDrive(drive, context);
            }
            catch (LoopFlowException)
            {
                throw;
            }
            catch (PipelineStoppedException)
            {
                throw;
            }
            catch (ActionPreferenceStopException)
            {
                throw;
            }
            catch (Exception e) // Catch-all OK, 3rd party callout
            {
                throw NewProviderInvocationException(
                    "RemoveDriveProviderException",
                    SessionStateStrings.RemoveDriveProviderException,
                    driveCmdletProvider.ProviderInfo,
                    null,
                    e);
            }

            if (result != null)
            {
                // Make sure the provider didn't try to pull a fast one on us
                // and substitute a different drive.

                if (string.Equals(result.Name, drive.Name, StringComparison.OrdinalIgnoreCase))
                {
                    driveRemovable = true;
                }
            }

            return driveRemovable;
        }

        #endregion RemoveDrive

        #region Drives

        /// <summary>
        /// Gets an enumerable list of the drives that are mounted in
        /// the specified scope.
        /// </summary>
        /// <param name="scope">
        /// The scope to retrieve the drives from. If null or empty,
        /// all drives from all scopes will be retrieved.
        /// </param>
        /// <exception cref="ArgumentException">
        /// If <paramref name="scope"/> is less than zero, or not
        /// a number and not "script", "global", "local", or "private"
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// If <paramref name="scopeID"/> is less than zero or greater than the number of currently
        /// active scopes.
        /// </exception>
        internal Collection<PSDriveInfo> Drives(string scope)
        {
            Dictionary<string, PSDriveInfo> driveTable = new Dictionary<string, PSDriveInfo>();

            SessionStateScope startingScope = _currentScope;

            if (!string.IsNullOrEmpty(scope))
            {
                startingScope = GetScopeByID(scope);
            }

            SessionStateScopeEnumerator scopeEnumerator =
                new SessionStateScopeEnumerator(startingScope);
            DriveInfo[] alldrives = DriveInfo.GetDrives();
            Collection<string> driveNames = new Collection<string>();
            foreach (DriveInfo drive in alldrives)
            {
                driveNames.Add(drive.Name.Substring(0, 1));
            }

            foreach (SessionStateScope lookupScope in scopeEnumerator)
            {
                foreach (PSDriveInfo drive in lookupScope.Drives)
                {
                    // It is the correct behavior for child scope
                    // drives to overwrite parent scope drives of
                    // the same name.

                    if (drive != null)
                    {
                        bool driveIsValid = true;

                        // If the drive is auto-mounted, ensure that it still exists, or remove the drive.
#if !UNIX
                        if (drive.IsAutoMounted || IsAStaleVhdMountedDrive(drive))
                        {
                            driveIsValid = ValidateOrRemoveAutoMountedDrive(drive, lookupScope);
                        }
#endif
                        if (drive.Name.Length == 1)
                        {
                            if (!(driveNames.Contains(drive.Name)))
                                driveTable.Remove(drive.Name);
                        }

                        if (driveIsValid && !driveTable.ContainsKey(drive.Name))
                        {
                            driveTable[drive.Name] = drive;
                        }
                    }
                }

                // If the scope was specified then don't loop
                // through the other scopes

                if (scope != null && scope.Length > 0)
                {
                    break;
                }
            }

            // Now lookup all the file system drives and automount any that are not
            // present

            try
            {
                foreach (System.IO.DriveInfo fsDriveInfo in alldrives)
                {
                    if (fsDriveInfo != null)
                    {
                        string fsDriveName = fsDriveInfo.Name.Substring(0, 1);
                        if (!driveTable.ContainsKey(fsDriveName))
                        {
                            PSDriveInfo automountedDrive = AutomountFileSystemDrive(fsDriveInfo);
                            if (automountedDrive != null)
                            {
                                driveTable[automountedDrive.Name] = automountedDrive;
                            }
                        }
                    }
                }
            }
            // We don't want to have automounting cause an exception. We
            // rather it just fail silently as it wasn't a result of an
            // explicit request by the user anyway.
            catch (IOException)
            {
            }
            catch (UnauthorizedAccessException)
            {
            }

            Collection<PSDriveInfo> results = new Collection<PSDriveInfo>();
            foreach (PSDriveInfo drive in driveTable.Values)
            {
                results.Add(drive);
            }

            return results;
        }

        #endregion Drives

        /// <summary>
        /// Gets or sets the current working drive.
        /// </summary>
        internal PSDriveInfo CurrentDrive
        {
            get
            {
                if (this != ExecutionContext.TopLevelSessionState)
                    return ExecutionContext.TopLevelSessionState.CurrentDrive;
                else
                    return _currentDrive;
            }

            set
            {
                if (this != ExecutionContext.TopLevelSessionState)
                    ExecutionContext.TopLevelSessionState.CurrentDrive = value;
                else
                    _currentDrive = value;
            }
        }
    }
}

#pragma warning restore 56500
