Skip to content

Commit 2ce9b1c

Browse files
committed
Better handling of long paths
By integrating the use of long path prefix (\?\\) and careful use of short paths we can workaround most of the MSI API long path limitations. It's not perfect as short paths that still exceed MAX_PATH will fail in most MSI APIs. But accessing files placed in cabinets and copied around should now be fully long path supported. Fixes 3065 9115
1 parent cf15a49 commit 2ce9b1c

21 files changed

+323
-41
lines changed

src/wix/WixToolset.Core.Burn/Bind/ProcessBundleSoftwareTagsCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ private static IEnumerable<SoftwareTag> CollectPackageTags(IntermediateSection s
6969
{
7070
var payload = payloadSymbolsById[msiPackage.PayloadRef];
7171

72-
using (var db = new Database(payload.SourceFile.Path, OpenDatabase.ReadOnly))
72+
using (var db = Database.OpenAsReadOnly(payload.SourceFile.Path))
7373
{
7474
if (db.TableExists("SoftwareIdentificationTag"))
7575
{

src/wix/WixToolset.Core.Burn/Bundles/ProcessMsiPackageCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public WixBundleHarvestedMsiPackageSymbol HarvestPackage()
164164

165165
this.CheckIfWindowsInstallerFileTooLarge(this.PackagePayload.SourceLineNumbers, sourcePath, "MSI");
166166

167-
using (var db = new Database(sourcePath, OpenDatabase.ReadOnly))
167+
using (var db = Database.OpenAsReadOnly(sourcePath))
168168
{
169169
// Read data out of the msi database...
170170
using (var sumInfo = new SummaryInformation(db))

src/wix/WixToolset.Core.Burn/Bundles/ProcessMspPackageCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ private WixBundleHarvestedMspPackageSymbol HarvestPackage()
9898

9999
try
100100
{
101-
using (var db = new Database(sourcePath, OpenDatabase.ReadOnly | OpenDatabase.OpenPatchFile))
101+
using (var db = Database.OpenAsReadOnly(sourcePath, asPatch: true))
102102
{
103103
// Read data out of the msp database...
104104
using (var sumInfo = new SummaryInformation(db))
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2+
3+
namespace WixToolset.Core.Native
4+
{
5+
using System.IO;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
8+
9+
internal static class PathUtil
10+
{
11+
private const int MaxPath = 260;
12+
private const string LongPathPrefix = @"\\?\";
13+
14+
public static bool CreateOrGetShortPath(string path, out string shortPath)
15+
{
16+
var fileCreated = false;
17+
18+
// The file must exist so we can get its short path.
19+
if (!File.Exists(path))
20+
{
21+
using (File.Create(path))
22+
{
23+
}
24+
25+
fileCreated = true;
26+
}
27+
28+
// Use the short path to avoid issues with long paths in the MSI API.
29+
shortPath = GetShortPath(path);
30+
31+
return fileCreated;
32+
}
33+
34+
public static string GetPrefixedLongPath(string path)
35+
{
36+
if (path.Length > MaxPath && !path.StartsWith(LongPathPrefix))
37+
{
38+
path = LongPathPrefix + path;
39+
}
40+
41+
return path;
42+
}
43+
44+
public static string GetShortPath(string longPath)
45+
{
46+
var path = GetPrefixedLongPath(longPath);
47+
48+
var buffer = new StringBuilder(MaxPath); // start with MAX_PATH.
49+
50+
var result = GetShortPathName(path, buffer, (uint)buffer.Capacity);
51+
52+
// If result > buffer.Capacity, reallocate and call again (even though we're usually using short names to avoid long path)
53+
// so the short path result is still going to end up too long for APIs requiring a short path.
54+
if (result > buffer.Capacity)
55+
{
56+
buffer = new StringBuilder((int)result);
57+
58+
result = GetShortPathName(path, buffer, (uint)buffer.Capacity);
59+
}
60+
61+
// If we succeeded, return the short path without the prefix.
62+
if (result > 0)
63+
{
64+
path = buffer.ToString();
65+
66+
if (path.StartsWith(LongPathPrefix))
67+
{
68+
path = path.Substring(LongPathPrefix.Length);
69+
}
70+
}
71+
72+
return path;
73+
}
74+
75+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
76+
private static extern uint GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, uint cchBuffer);
77+
}
78+
}

src/wix/WixToolset.Core.Native/Msi/Database.cs

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public sealed class Database : MsiHandle
1818
/// </summary>
1919
/// <param name="path">Path to the database to be opened.</param>
2020
/// <param name="type">Persist mode to use when opening the database.</param>
21-
public Database(string path, OpenDatabase type)
21+
private Database(string path, OpenDatabase type)
2222
{
2323
var error = MsiInterop.MsiOpenDatabase(path, (IntPtr)type, out var handle);
2424
if (0 != error)
@@ -34,15 +34,90 @@ public Database(string path, OpenDatabase type)
3434
/// </summary>
3535
public static int MsiMaxStreamNameLength => MsiInterop.MsiMaxStreamNameLength;
3636

37+
/// <summary>
38+
/// Creates a new <see cref="Database"/> with the specified path.
39+
/// </summary>
40+
/// <param name="path">Path of database to be created.</param>
41+
/// <param name="asPatch">Indicates whether the database should be opened as a patch file.</param>
42+
public static Database Create(string path, bool asPatch = false)
43+
{
44+
var fileCreated = false;
45+
var mode = OpenDatabase.CreateDirect;
46+
47+
if (asPatch)
48+
{
49+
mode |= OpenDatabase.OpenPatchFile;
50+
}
51+
52+
try
53+
{
54+
fileCreated = PathUtil.CreateOrGetShortPath(path, out var shortPath);
55+
56+
return new Database(shortPath, mode);
57+
}
58+
catch // cleanup on error if we created the short path file.
59+
{
60+
if (fileCreated)
61+
{
62+
File.Delete(path);
63+
}
64+
65+
throw;
66+
}
67+
}
68+
69+
/// <summary>
70+
/// Opens an existing <see cref="Database"/> with the specified path.
71+
/// </summary>
72+
/// <param name="path">Path of database to open.</param>
73+
/// <param name="transact">Indicates whether to open the database in transaction mode.</param>
74+
/// <param name="asPatch">Indicates whether the database should be opened as a patch file.</param>
75+
public static Database Open(string path, bool transact = false, bool asPatch = false)
76+
{
77+
var mode = transact ? OpenDatabase.Transact : OpenDatabase.Direct;
78+
79+
if (asPatch)
80+
{
81+
mode |= OpenDatabase.OpenPatchFile;
82+
}
83+
84+
// Use the short path to avoid issues with long paths in the MSI API.
85+
var shortPath = PathUtil.GetShortPath(path);
86+
87+
return new Database(shortPath, mode);
88+
}
89+
90+
/// <summary>
91+
/// Opens an existing <see cref="Database"/> with the specified path.
92+
/// </summary>
93+
/// <param name="path">Path of database to open.</param>
94+
/// <param name="asPatch">Indicates whether the database should be opened as a patch file.</param>
95+
public static Database OpenAsReadOnly(string path, bool asPatch = false)
96+
{
97+
var mode = OpenDatabase.ReadOnly;
98+
99+
if (asPatch)
100+
{
101+
mode |= OpenDatabase.OpenPatchFile;
102+
}
103+
104+
// Use the short path to avoid issues with long paths in the MSI API.
105+
var shortPath = PathUtil.GetShortPath(path);
106+
107+
return new Database(shortPath, mode);
108+
}
109+
37110
/// <summary>
38111
/// Apply a transform to the MSI.
39112
/// </summary>
40113
/// <param name="transformFile">Path to transform to apply.</param>
41114
public void ApplyTransform(string transformFile)
42115
{
116+
var shortTransformFile = PathUtil.GetShortPath(transformFile);
117+
43118
// get the curret validation bits
44119
var conditions = TransformErrorConditions.None;
45-
using (var summaryInfo = new SummaryInformation(transformFile))
120+
using (var summaryInfo = new SummaryInformation(shortTransformFile))
46121
{
47122
try
48123
{
@@ -65,7 +140,9 @@ public void ApplyTransform(string transformFile)
65140
/// <param name="errorConditions">Specifies the error conditions that are to be suppressed.</param>
66141
public void ApplyTransform(string transformFile, TransformErrorConditions errorConditions)
67142
{
68-
var error = MsiInterop.MsiDatabaseApplyTransform(this.Handle, transformFile, errorConditions);
143+
var shortTransformFile = PathUtil.GetShortPath(transformFile);
144+
145+
var error = MsiInterop.MsiDatabaseApplyTransform(this.Handle, shortTransformFile, errorConditions);
69146
if (0 != error)
70147
{
71148
throw new MsiException(error);
@@ -119,7 +196,9 @@ public void Commit()
119196
/// shows which properties should be validated to verify that this transform can be applied to the database.</param>
120197
public void CreateTransformSummaryInfo(Database referenceDatabase, string transformFile, TransformErrorConditions errorConditions, TransformValidations validations)
121198
{
122-
var error = MsiInterop.MsiCreateTransformSummaryInfo(this.Handle, referenceDatabase.Handle, transformFile, errorConditions, validations);
199+
var shortTransformFile = PathUtil.GetShortPath(transformFile);
200+
201+
var error = MsiInterop.MsiCreateTransformSummaryInfo(this.Handle, referenceDatabase.Handle, shortTransformFile, errorConditions, validations);
123202
if (0 != error)
124203
{
125204
throw new MsiException(error);
@@ -137,7 +216,9 @@ public void Import(string idtPath)
137216
var folderPath = Path.GetFullPath(Path.GetDirectoryName(idtPath));
138217
var fileName = Path.GetFileName(idtPath);
139218

140-
var error = MsiInterop.MsiDatabaseImport(this.Handle, folderPath, fileName);
219+
var shortFolderPath = PathUtil.GetShortPath(folderPath);
220+
221+
var error = MsiInterop.MsiDatabaseImport(this.Handle, shortFolderPath, fileName);
141222
if (1627 == error) // ERROR_FUNCTION_FAILED
142223
{
143224
throw new WixInvalidIdtException(idtPath);
@@ -161,7 +242,9 @@ public void Export(string tableName, string folderPath, string fileName)
161242
folderPath = Environment.CurrentDirectory;
162243
}
163244

164-
var error = MsiInterop.MsiDatabaseExport(this.Handle, tableName, folderPath, fileName);
245+
var shortFolderPath = PathUtil.GetShortPath(folderPath);
246+
247+
var error = MsiInterop.MsiDatabaseExport(this.Handle, tableName, shortFolderPath, fileName);
165248
if (0 != error)
166249
{
167250
throw new MsiException(error);
@@ -177,13 +260,29 @@ public void Export(string tableName, string folderPath, string fileName)
177260
/// there are no differences between the two databases.</returns>
178261
public bool GenerateTransform(Database referenceDatabase, string transformFile)
179262
{
180-
var error = MsiInterop.MsiDatabaseGenerateTransform(this.Handle, referenceDatabase.Handle, transformFile, 0, 0);
181-
if (0 != error && 0xE8 != error) // ERROR_NO_DATA(0xE8) means no differences were found
263+
var fileCreated = false;
264+
265+
try
182266
{
183-
throw new MsiException(error);
267+
fileCreated = PathUtil.CreateOrGetShortPath(transformFile, out var shortTransformFile);
268+
269+
var error = MsiInterop.MsiDatabaseGenerateTransform(this.Handle, referenceDatabase.Handle, shortTransformFile, 0, 0);
270+
if (0 != error && 0xE8 != error) // ERROR_NO_DATA(0xE8) means no differences were found
271+
{
272+
throw new MsiException(error);
273+
}
274+
275+
return (0xE8 != error);
184276
}
277+
catch // Cleanup on error
278+
{
279+
if (fileCreated)
280+
{
281+
File.Delete(transformFile);
282+
}
185283

186-
return (0xE8 != error);
284+
throw;
285+
}
187286
}
188287

189288
/// <summary>

src/wix/WixToolset.Core.Native/Msi/Installer.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ public static string ExtractPatchXml(string path)
3434
var buffer = new StringBuilder(65535);
3535
var size = buffer.Capacity;
3636

37-
var error = MsiInterop.MsiExtractPatchXMLData(path, 0, buffer, ref size);
37+
var shortPath = PathUtil.GetShortPath(path);
38+
39+
var error = MsiInterop.MsiExtractPatchXMLData(shortPath, 0, buffer, ref size);
3840
if (234 == error)
3941
{
4042
buffer.EnsureCapacity(++size);
41-
error = MsiInterop.MsiExtractPatchXMLData(path, 0, buffer, ref size);
43+
error = MsiInterop.MsiExtractPatchXMLData(shortPath, 0, buffer, ref size);
4244
}
4345

4446
if (error != 0)
@@ -57,8 +59,12 @@ public static string ExtractPatchXml(string path)
5759
/// <param name="hash">Int array that receives the returned file hash information.</param>
5860
public static void GetFileHash(string filePath, int options, out int[] hash)
5961
{
60-
var hashInterop = new MSIFILEHASHINFO();
61-
hashInterop.FileHashInfoSize = 20;
62+
var hashInterop = new MSIFILEHASHINFO
63+
{
64+
FileHashInfoSize = 20
65+
};
66+
67+
filePath = PathUtil.GetPrefixedLongPath(filePath);
6268

6369
var error = MsiInterop.MsiGetFileHash(filePath, Convert.ToUInt32(options), hashInterop);
6470
if (0 != error)
@@ -76,9 +82,9 @@ public static void GetFileHash(string filePath, int options, out int[] hash)
7682
}
7783

7884
/// <summary>
79-
/// Returns the version string and language string in the format that the installer
80-
/// expects to find them in the database. If you just want version information, set
81-
/// lpLangBuf and pcchLangBuf to zero. If you just want language information, set
85+
/// Returns the version string and language string in the format that the installer
86+
/// expects to find them in the database. If you just want version information, set
87+
/// lpLangBuf and pcchLangBuf to zero. If you just want language information, set
8288
/// lpVersionBuf and pcchVersionBuf to zero.
8389
/// </summary>
8490
/// <param name="filePath">Specifies the path to the file.</param>
@@ -91,6 +97,8 @@ public static void GetFileVersion(string filePath, out string version, out strin
9197
var versionBuffer = new StringBuilder(versionLength);
9298
var languageBuffer = new StringBuilder(languageLength);
9399

100+
filePath = PathUtil.GetPrefixedLongPath(filePath);
101+
94102
var error = MsiInterop.MsiGetFileVersion(filePath, versionBuffer, ref versionLength, languageBuffer, ref languageLength);
95103
if (234 == error)
96104
{

src/wix/WixToolset.Core.Native/Msi/OpenDatabase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
22

33
namespace WixToolset.Core.Native.Msi
44
{

src/wix/WixToolset.Core.Native/Msi/SummaryInformation.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,10 @@ public SummaryInformation(string databaseFile)
220220
throw new ArgumentNullException(nameof(databaseFile));
221221
}
222222

223+
var shortDatabaseFile = PathUtil.GetShortPath(databaseFile);
224+
223225
var handle = IntPtr.Zero;
224-
var error = MsiInterop.MsiGetSummaryInformation(IntPtr.Zero, databaseFile, 0, ref handle);
226+
var error = MsiInterop.MsiGetSummaryInformation(IntPtr.Zero, shortDatabaseFile, 0, ref handle);
225227
if (0 != error)
226228
{
227229
throw new MsiException(error);

src/wix/WixToolset.Core.Native/WindowsInstallerValidator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ private void RunValidations()
9393

9494
try
9595
{
96-
using (var database = new Database(this.DatabasePath, OpenDatabase.Direct))
96+
using (var database = Database.Open(this.DatabasePath))
9797
{
9898
var propertyTableExists = database.TableExists("Property");
9999
string productCode = null;
@@ -130,7 +130,7 @@ private void RunValidations()
130130

131131
try
132132
{
133-
using (var cubeDatabase = new Database(findCubeFile.Path, OpenDatabase.ReadOnly))
133+
using (var cubeDatabase = Database.OpenAsReadOnly(findCubeFile.Path))
134134
{
135135
try
136136
{

src/wix/WixToolset.Core.WindowsInstaller/Bind/BindTransformCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,8 +418,8 @@ public void Execute()
418418
Directory.CreateDirectory(Path.GetDirectoryName(this.OutputPath));
419419

420420
// create the transform file
421-
using (var targetDatabase = new Database(targetDatabaseFile, OpenDatabase.ReadOnly))
422-
using (var updatedDatabase = new Database(updatedDatabaseFile, OpenDatabase.ReadOnly))
421+
using (var targetDatabase = Database.OpenAsReadOnly(targetDatabaseFile))
422+
using (var updatedDatabase = Database.OpenAsReadOnly(updatedDatabaseFile))
423423
{
424424
if (updatedDatabase.GenerateTransform(targetDatabase, this.OutputPath))
425425
{

0 commit comments

Comments
 (0)