Nexus-Mods / Nexus-Mod-Manager

GNU General Public License v2.0
972 stars 165 forks source link

System.IO.Path.CheckInvalidPathChars not handled in Nexus.Client.Util.FileUtil.RelativizePath #1018

Open IngwiePhoenix opened 4 years ago

IngwiePhoenix commented 4 years ago

Describe the bug While starting NMM, one of the mods causes CheckInvalidPathChars to be thrown but not to be handled at all. The resulting traceback also does not indicate which file exactly caused the issue - thus, it is impossible to figure out what exactly caused this. I am not the actual reporter since I use Vortex with SkyrimVR and my friend is the one using NMM with Skyrim SE. However, I was given the traces, off which I had him move a few files around to verify my thesis.

Environment

To Reproduce The game has been moved between harddrives and OS versions. A clean reproduction is thus not possible and this may be treated as an edge-case. However, the fix is very easy to do! :)

Snippet from the trace: (removing a few information as to strip the length and highlight the more important aspects)

            Found: C:\Games\The Elder Scrolls V Skyrim Special Edition\Data\0AAA.esp
            Problem encountered when parsing official plugin file names, see information below.
            Exception: 
Message: 
    Illegal characters in path.
Full Trace: 
    System.ArgumentException: Illegal characters in path.
   at System.IO.Path.CheckInvalidPathChars(String path, Boolean checkAdditional)
   at System.IO.Path.Combine(String path1, String path2)
   at Nexus.Client.Games.Gamebryo.GamebryoGameModeDescriptorBase.get_OrderedOfficialUnmanagedPluginNames()

                Registered.
            Found: C:\Games\The Elder Scrolls V Skyrim Special Edition\Data\Alexs Higher Bounty Rewards EXTRA.esp

            Tracing an Unhandled Exception:
            Exception: 
Message: 
    Value cannot be null.
Parameter name: p_strRoot
Full Trace: 
    System.ArgumentNullException: Value cannot be null.
Parameter name: p_strRoot
   at Nexus.Client.Util.FileUtil.RelativizePath(String p_strRoot, String p_strPath)
   at Nexus.Client.Games.Gamebryo.PluginManagement.LoadOrder.PluginOrderManager.StripPluginDirectory(String[] p_strPlugins)
   at Nexus.Client.Games.Gamebryo.PluginManagement.LoadOrder.PluginOrderManager.IsPluginActive(String p_strPlugin)
   at Nexus.Client.Games.Gamebryo.PluginManagement.GamebryoPluginFactory.CreatePlugin(String p_strPluginPath)
   at Nexus.Client.PluginManagement.PluginRegistry.TransactionEnlistment.RegisterPlugin(String p_strPluginPath)
   at Nexus.Client.PluginManagement.PluginRegistry.DiscoverManagedPlugins(IPluginFactory p_pftFactory, IPluginDiscoverer p_pdvDiscoverer)
   at Nexus.Client.ApplicationInitializer.InitializeServices(IGameMode p_gmdGameMode, IModRepository p_mrpModRepository, NexusFileUtil p_nfuFileUtility, SynchronizationContext p_scxUIContext, ViewMessage& p_vwmErrorMessage)
   at Nexus.Client.ApplicationInitializer.DoApplicationInitialize(IGameModeFactory p_gmfGameModeFactory, SynchronizationContext p_scxUIContext, ViewMessage& p_vwmErrorMessage)
   at Nexus.Client.ApplicationInitializer.DoWork(Object[] args)
   at Nexus.Client.ThreadedBackgroundTask.RunThreadedWork(Object p_objArgs)
   at Nexus.Client.Util.Threading.TrackedThread.RunParameterizedThread(Object p_objParam)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart(Object obj)

            Running Threads (1)
    3 () 
        Aborted

Solution: catch(System.ArgumentException) and either create a warning prompt with which path is broken, or skip it alltogether if possible. This should also give room to properly pass p_strPlugin.

IngwiePhoenix commented 4 years ago

I spent some time and traced it down and got a few interesting results out of it.

First, I modified GamebryoGameModeDescriptorBase::OrderedOfficialUnmanagedPluginNames { get } to actually print me something useful:

        public override string[] OrderedOfficialUnmanagedPluginNames
        {
            get
            {
                try
                {
                    if (_officialUnmanagedPlugins == null)
                    {
                        if (OrderedOfficialUnmanagedPluginFilenames != null)
                        {
                            _officialUnmanagedPlugins = new string[OrderedOfficialUnmanagedPluginFilenames.Length];

                            for (var i = OrderedOfficialUnmanagedPluginFilenames.Length - 1; i >= 0; i--)
                            {
                                try
                                {
                                    string p = Path.Combine(PluginDirectory, OrderedOfficialUnmanagedPluginFilenames[i]).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
                                    _officialUnmanagedPlugins[i] = p;
                                }
                                catch (ArgumentException e)
                                {
                                    Trace.TraceError("An error occured while reading an internal mod file.");
                                    Trace.TraceError("Arguments:");
                                    Trace.TraceError("PluginDirectory = " + PluginDirectory);
                                    Trace.TraceError("OrderedOfficialUnmanagedPluginFilenames[i] = " + OrderedOfficialUnmanagedPluginFilenames[i]);
                                }

                            }

                            return _officialUnmanagedPlugins;
                        }
                        else
                        {
                            return new string[0];
                        }
                    }

                    return _officialUnmanagedPlugins;
                }
                catch (ArgumentException e)
                {
                    Trace.TraceError("Problem encountered when parsing official plugin file names, see information below.");
                    TraceUtil.TraceException(e);

                    return new string[0];
                }
            }
        }

Then I traced the calls and instantiation to this method and class and found it in SkyrimSEGameModeDescriptor. From there, I noticed that it was reading Skyrim.ccc. So I compared this version with an older one. And yes - NMM works on the older version, but not on the newer one. That means the internal format of it changed, which ultimatively delivered this on a perfectly clean install:

            An error occured while reading an internal mod file.
            Arguments:
            PluginDirectory = C:\Games\The Elder Scrolls V Skyrim Special Edition\Data
            OrderedOfficialUnmanagedPluginFilenames[i] = �%~�s�Sgu(
            An error occured while reading an internal mod file.
            Arguments:
            PluginDirectory = C:\Games\The Elder Scrolls V Skyrim Special Edition\Data
            OrderedOfficialUnmanagedPluginFilenames[i] = ���P��x�j�A?�0��1�����Y

So, to fix this issue, you may want to look at the various versions of Skyrim.ccc :)