2

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.

Anodyne
  • 1,760
  • 2
  • 15
  • 28
  • I suppose the issue is not related to VSTO at all, it is a pure .net question. Do you get the same error with a windows forms or console application? – Eugene Astafiev Apr 20 '22 at 22:04
  • Does this answer your question? [How to deal with files with a name longer than 259 characters?](https://stackoverflow.com/questions/5188527/how-to-deal-with-files-with-a-name-longer-than-259-characters) – Eugene Astafiev Apr 20 '22 at 22:06
  • As I stated in the question, I was able to get it working (using the `\\?\` prefix) in a console application. I think it might be a good idea if I revised the question to make that clearer, as ideally I'm looking for a way to get long paths in the add-in behaving the same as they do in a console app. – Anodyne Apr 21 '22 at 10:23
  • I've revised the question to clarify that I'm mainly interested in the difference in behaviour between the same code running in the VSTO add-in vs a console app. – Anodyne Apr 21 '22 at 11:28

1 Answers1

1

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.

Anodyne
  • 1,760
  • 2
  • 15
  • 28
Eugene Astafiev
  • 47,483
  • 3
  • 24
  • 45
  • As I said in the question: "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)". The config file is definitely being read OK, because my add-in can read settings from it, but the AppContextSwitchOverrides element has no effect whatsoever. – Anodyne Apr 21 '22 at 09:54
  • You need to use a config file for the host application, not add-in. – Eugene Astafiev Apr 21 '22 at 10:03
  • Apologies - I downvoted your answer because it seemed you hadn't read the question properly, although on reflection I could probably have made the question clearer. I'll think about that now and possibly revise it. I can't withdraw my downvote now but if you edit your answer I'll take it off :) – Anodyne Apr 21 '22 at 10:17
  • Regarding the config file for the host application, I'm not sure how that's supposed to work. Putting a POWERPNT.EXE.config in the same directory as my add-in doesn't work, as PowerPoint loads long before my add-in. Are you saying I need to create a POWERPNT.EXE.config and place it in the same directory as POWERPNT.EXE when the add-in is installed? That seems highly dubious. – Anodyne Apr 21 '22 at 10:20
  • Correct. The config file should belong to the host application. – Eugene Astafiev Apr 21 '22 at 11:45
  • I've corrected the post. – Eugene Astafiev Apr 21 '22 at 11:46
  • Thank you Eugene! I've just tried this, and it seems that setting AppContextSwitchOverrides in the POWERPNT.EXE.config file has extra magical powers that you don't get if you use AppContext.SetSwitch() to do the same thing at runtime. – Anodyne Apr 21 '22 at 12:21
  • Then it makes sense to mark the answer. – Eugene Astafiev Apr 21 '22 at 12:22
  • I can't actually use that solution as I'm repulsed by the idea of dropping files into someone else's installation directory, but it does look like that might be the only way to get the behaviour to match what we see in a console app. I'm experimenting some more but I'll accept your answer shortly if I don't come up with anything better :) – Anodyne Apr 21 '22 at 12:31