Home > Software design >  Why can I open a file with a long (>259 characters) path from a console app, but not from a Power
Why can I open a file with a long (>259 characters) path from a console app, but not from a Power

Time:04-22

We have an add-in for PowerPoint built using Visual Studio Tools for Office (VSTO) in C# with VS2019, targeting .NET Framework 4.7.2. Occasionally this add-in needs to open a file specified by the user, which it does using the usual System.IO.File.Open API. We've recently had an error report from a customer who couldn't get it to open one of their files. They included a stack trace, which looked something like this:

System.IO.PathTooLongException: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.
   at System.IO.PathHelper.GetFullPathName()
   at System.IO.Path.LegacyNormalizePath(String path, Boolean fullCheck, Int32 maxPathLength, Boolean expandShortPaths)
   at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength, Boolean expandShortPaths)
   at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength)
   at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
   at System.IO.File.Open(String path, FileMode mode, FileAccess access, FileShare share)

It turns out the path to their file was longer than the old 259-character limit. Annoyingly, we couldn't even work around it by using the "short" path to the file (i.e. with each path component replaced with the legacy 8.3 version). That path was well under the limit, but we got the same exception when we tried to open it, with a slightly different stack trace:

System.IO.PathTooLongException: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.
   at System.IO.PathHelper.TryExpandShortFileName()
   at System.IO.Path.LegacyNormalizePath(String path, Boolean fullCheck, Int32 maxPathLength, Boolean expandShortPaths)
   at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength, Boolean expandShortPaths)
   at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength)
   at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
   at System.IO.File.Open(String path, FileMode mode, FileAccess access, FileShare share)

So it looks like, in order to open the file, it's expanding the short name to the long name first, then complaining that it's too long. Seems unnecessary when the file can be opened perfectly fine using its short name, but OK - we just need another solution.

The next thing I tried was using the full path with the \\?\ "raw" path prefix, which has always got me out of jail in the past (albeit in C code using the Windows API directly). Still no joy - I got the same exception with exactly the same stack trace as I did with the full path sans prefix.

After a bit of Googling I found this page documenting the AppContextSwitchOverrides element in app.config - the switches that particularly caught my interest were Switch.System.IO.UseLegacyPathHandling and Switch.System.IO.BlockLongPaths. I checked during my add-in's startup to see what values those switches had (using AppContext.TryGetSwitch) and both were set to true, so I thought I'd try turning them both off. Adding the element to my app.config didn't work however (presumably because the add-in isn't its own app, but is being hosted inside PowerPoint). So I used the following snippet of code in my add-in before attempting to open any files:

AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);

Then I tried opening the file using each of the paths I mentioned above (full, short, and full with \\?\ prefix) and... I got exactly the same exceptions as before, with exactly the same stack traces in each case.

For those of you who haven't yet lost the will to live, this is what the relevant bits of my ThisAddIn.cs class look like at this point:

using System;
using System.Diagnostics;
using System.IO;

static void TryOpen(string path, string desc)
{
  try
  {
    using (var strm = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None))
    {
    }
  }
  catch (IOException ex)
  {
    Debug.WriteLine($"Opening {desc} path failed with exception: {ex}");
  }
}

private void ThisAddIn_Startup(object sender, EventArgs e)
{
  AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
  AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);
  var shortPath = @"D:\LONGDI~1\LONGDI~1\LONGDI~1\LONGDI~1\LONGDI~1\test.txt";
  var longPath = @"D:\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\test.txt";
  var prefixedPath = @"\\?\D:\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\test.txt";
  TryOpen(longPath, "long");
  TryOpen(shortPath, "short");
  TryOpen(prefixedPath, "prefixed");
}

I've also tried using the same code in a console app targeting .NET Framework 4.7.2 and the "long" and "short" paths fail there as well, but the "prefixed" path works just fine. In fact, in the console app I can omit the AppContext.SetSwitch() calls and I get exactly the same behaviour ("prefixed" is the only one that works).

I've actually found a solution, but I hate it. If I always use the \\?\ prefix and use the Windows CreateFile() API via P/Invoke, that gives me a file handle that I can pass to the FileStream constructor. I could make that work, but I'd have to update the code everywhere a file is opened, making sure the new code behaves the same way in error cases as well as on the "happy path". It's doable but it's a few days' work that I'd rather avoid if possible, and the fact that the same code works fine in a console app makes me think there must be a way to get it working without all of that.

So my question is: has anyone else come across this issue, and is there a way to coax System.IO.File.Open() into accepting long paths when running in a VSTO PowerPoint add-in? I'm fine with the same level of functionality I get in a console app (i.e. being able to get it working by adding the \\?\ prefix) and the fact that I can get it working in a console app makes me think there must be some way to do it that I'm currently missing.

UPDATE: I give up. I've accepted Eugene's answer because it might work for some people - if you're building an internal application and you have complete control over the target environment, it might be appropriate to deploy app config files for Office executables. I can't ship a commercial app that does that, though. I'll check out the available .NET libraries offering support for long paths, or roll my own using P/Invoke if none of them work for me.

CodePudding user response:

The simplest way is to use a config file for the host application (i.e. in this case, a file named POWERPNT.EXE.config, placed in the same directory as POWERPNT.EXE) with the following content:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false;Switch.System.IO.BlockLongPaths=false" />
  </runtime>
</configuration>

You can find possible solutions described in the How to deal with files with a name longer than 259 characters? thread.

  • Related