Skip to content

Commit a93882f

Browse files
Issue8273 corrupt nu get cache (#8275)
Fixes #8273 Context Prevent overwriting the source of a hard/symbolic link. Changes Made Always delete the destination file (unless readonly and OverwriteReadOnlyFiles is false) Testing Added unit test.
1 parent 18fe510 commit a93882f

File tree

3 files changed

+112
-32
lines changed

3 files changed

+112
-32
lines changed

src/Tasks.UnitTests/Copy_Tests.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2096,6 +2096,87 @@ public void InvalidErrorIfLinkFailed()
20962096
Assert.False(result);
20972097
engine.AssertLogContains("MSB3892");
20982098
}
2099+
2100+
/// <summary>
2101+
/// An existing link source should not be modified.
2102+
/// </summary>
2103+
/// <remarks>
2104+
/// Related to issue [#8273](https://github.com/dotnet/msbuild/issues/8273)
2105+
/// </remarks>
2106+
[Theory]
2107+
[InlineData(false, false)]
2108+
[InlineData(false, true)]
2109+
[InlineData(true, false)]
2110+
public void DoNotCorruptSourceOfLink(bool useHardLink, bool useSymbolicLink)
2111+
{
2112+
string sourceFile1 = FileUtilities.GetTemporaryFile();
2113+
string sourceFile2 = FileUtilities.GetTemporaryFile();
2114+
string temp = Path.GetTempPath();
2115+
string destFolder = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A398");
2116+
string destFile = Path.Combine(destFolder, "The Destination");
2117+
2118+
try
2119+
{
2120+
File.WriteAllText(sourceFile1, "This is the first source temp file."); // HIGHCHAR: Test writes in UTF8 without preamble.
2121+
File.WriteAllText(sourceFile2, "This is the second source temp file."); // HIGHCHAR: Test writes in UTF8 without preamble.
2122+
2123+
// Don't create the dest folder, let task do that
2124+
2125+
ITaskItem[] sourceFiles = { new TaskItem(sourceFile1) };
2126+
ITaskItem[] destinationFiles = { new TaskItem(destFile) };
2127+
2128+
var me = new MockEngine(true);
2129+
var t = new Copy
2130+
{
2131+
RetryDelayMilliseconds = 1, // speed up tests!
2132+
BuildEngine = me,
2133+
SourceFiles = sourceFiles,
2134+
DestinationFiles = destinationFiles,
2135+
SkipUnchangedFiles = true,
2136+
UseHardlinksIfPossible = useHardLink,
2137+
UseSymboliclinksIfPossible = useSymbolicLink,
2138+
};
2139+
2140+
bool success = t.Execute();
2141+
2142+
Assert.True(success); // "success"
2143+
Assert.True(File.Exists(destFile)); // "destination exists"
2144+
2145+
string destinationFileContents = File.ReadAllText(destFile);
2146+
Assert.Equal("This is the first source temp file.", destinationFileContents);
2147+
2148+
sourceFiles = new TaskItem[] { new TaskItem(sourceFile2) };
2149+
2150+
t = new Copy
2151+
{
2152+
RetryDelayMilliseconds = 1, // speed up tests!
2153+
BuildEngine = me,
2154+
SourceFiles = sourceFiles,
2155+
DestinationFiles = destinationFiles,
2156+
SkipUnchangedFiles = true,
2157+
UseHardlinksIfPossible = false,
2158+
UseSymboliclinksIfPossible = false,
2159+
};
2160+
2161+
success = t.Execute();
2162+
2163+
Assert.True(success); // "success"
2164+
Assert.True(File.Exists(destFile)); // "destination exists"
2165+
2166+
destinationFileContents = File.ReadAllText(destFile);
2167+
Assert.Equal("This is the second source temp file.", destinationFileContents);
2168+
2169+
// Read the source file (it should not have been overwritten)
2170+
string sourceFileContents = File.ReadAllText(sourceFile1);
2171+
Assert.Equal("This is the first source temp file.", sourceFileContents);
2172+
2173+
((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries
2174+
}
2175+
finally
2176+
{
2177+
Helpers.DeleteFiles(sourceFile1, sourceFile2, destFile);
2178+
}
2179+
}
20992180
}
21002181

21012182
public class CopyHardLink_Tests : Copy_Tests

src/Tasks/Copy.cs

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,6 @@ private void LogDiagnostic(string message, params object[] messageArgs)
226226
FileState sourceFileState, // The source file
227227
FileState destinationFileState) // The destination file
228228
{
229-
bool destinationFileExists = false;
230-
231229
if (destinationFileState.DirectoryExists)
232230
{
233231
Log.LogErrorWithCodeFromResources("Copy.DestinationIsDirectory", sourceFileState.Name, destinationFileState.Name);
@@ -269,7 +267,14 @@ private void LogDiagnostic(string message, params object[] messageArgs)
269267
if (OverwriteReadOnlyFiles)
270268
{
271269
MakeFileWriteable(destinationFileState, true);
272-
destinationFileExists = destinationFileState.FileExists;
270+
}
271+
272+
// If the destination file is a hard or symbolic link, File.Copy would overwrite the source.
273+
// To prevent this, we need to delete the existing entry before we Copy or create a link.
274+
// We could try to figure out if the file is a link, but I can't think of a reason to not simply delete it always.
275+
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_6) && destinationFileState.FileExists && !destinationFileState.IsReadOnly)
276+
{
277+
FileUtilities.DeleteNoThrow(destinationFileState.Name);
273278
}
274279

275280
bool symbolicLinkCreated = false;
@@ -279,7 +284,7 @@ private void LogDiagnostic(string message, params object[] messageArgs)
279284
// Create hard links if UseHardlinksIfPossible is true
280285
if (UseHardlinksIfPossible)
281286
{
282-
TryCopyViaLink(HardLinkComment, MessageImportance.Normal, sourceFileState, destinationFileState, ref destinationFileExists, out hardLinkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethods.MakeHardLink(destination, source, ref errorMessage, Log));
287+
TryCopyViaLink(HardLinkComment, MessageImportance.Normal, sourceFileState, destinationFileState, out hardLinkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethods.MakeHardLink(destination, source, ref errorMessage, Log));
283288
if (!hardLinkCreated)
284289
{
285290
if (UseSymboliclinksIfPossible)
@@ -297,13 +302,14 @@ private void LogDiagnostic(string message, params object[] messageArgs)
297302
// Create symbolic link if UseSymboliclinksIfPossible is true and hard link is not created
298303
if (!hardLinkCreated && UseSymboliclinksIfPossible)
299304
{
300-
TryCopyViaLink(SymbolicLinkComment, MessageImportance.Normal, sourceFileState, destinationFileState, ref destinationFileExists, out symbolicLinkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethodsShared.MakeSymbolicLink(destination, source, ref errorMessage));
301-
if (!NativeMethodsShared.IsWindows)
302-
{
303-
errorMessage = Log.FormatResourceString("Copy.NonWindowsLinkErrorMessage", "symlink()", errorMessage);
304-
}
305+
TryCopyViaLink(SymbolicLinkComment, MessageImportance.Normal, sourceFileState, destinationFileState, out symbolicLinkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethodsShared.MakeSymbolicLink(destination, source, ref errorMessage));
305306
if (!symbolicLinkCreated)
306307
{
308+
if (!NativeMethodsShared.IsWindows)
309+
{
310+
errorMessage = Log.FormatResourceString("Copy.NonWindowsLinkErrorMessage", "symlink()", errorMessage);
311+
}
312+
307313
Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.Name, destinationFileState.Name, errorMessage);
308314
}
309315
}
@@ -324,41 +330,28 @@ private void LogDiagnostic(string message, params object[] messageArgs)
324330
Log.LogMessage(MessageImportance.Normal, FileComment, sourceFilePath, destinationFilePath);
325331

326332
File.Copy(sourceFileState.Name, destinationFileState.Name, true);
333+
334+
// If the destinationFile file exists, then make sure it's read-write.
335+
// The File.Copy command copies attributes, but our copy needs to
336+
// leave the file writeable.
337+
if (sourceFileState.IsReadOnly)
338+
{
339+
destinationFileState.Reset();
340+
MakeFileWriteable(destinationFileState, false);
341+
}
327342
}
328343

329344
// Files were successfully copied or linked. Those are equivalent here.
330345
WroteAtLeastOneFile = true;
331346

332-
destinationFileState.Reset();
333-
334-
// If the destinationFile file exists, then make sure it's read-write.
335-
// The File.Copy command copies attributes, but our copy needs to
336-
// leave the file writeable.
337-
if (sourceFileState.IsReadOnly)
338-
{
339-
MakeFileWriteable(destinationFileState, false);
340-
}
341-
342347
return true;
343348
}
344349

345-
private void TryCopyViaLink(string linkComment, MessageImportance messageImportance, FileState sourceFileState, FileState destinationFileState, ref bool destinationFileExists, out bool linkCreated, ref string errorMessage, Func<string, string, string, bool> createLink)
350+
private void TryCopyViaLink(string linkComment, MessageImportance messageImportance, FileState sourceFileState, FileState destinationFileState, out bool linkCreated, ref string errorMessage, Func<string, string, string, bool> createLink)
346351
{
347352
// Do not log a fake command line as well, as it's superfluous, and also potentially expensive
348353
Log.LogMessage(MessageImportance.Normal, linkComment, sourceFileState.Name, destinationFileState.Name);
349354

350-
if (!OverwriteReadOnlyFiles)
351-
{
352-
destinationFileExists = destinationFileState.FileExists;
353-
}
354-
355-
// CreateHardLink and CreateSymbolicLink cannot overwrite an existing file or link
356-
// so we need to delete the existing entry before we create the hard or symbolic link.
357-
if (destinationFileExists)
358-
{
359-
FileUtilities.DeleteNoThrow(destinationFileState.Name);
360-
}
361-
362355
linkCreated = createLink(sourceFileState.Name, destinationFileState.Name, errorMessage);
363356
}
364357

@@ -826,6 +819,11 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF
826819
LogDiagnostic("Retrying on ERROR_ACCESS_DENIED because MSBUILDALWAYSRETRY = 1");
827820
}
828821
}
822+
else if (code == NativeMethods.ERROR_INVALID_FILENAME)
823+
{
824+
// Invalid characters used in file name, no point retrying.
825+
throw;
826+
}
829827

830828
if (e is UnauthorizedAccessException)
831829
{

src/Tasks/NativeMethods.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ internal static class NativeMethods
537537

538538
internal const int HRESULT_E_CLASSNOTREGISTERED = -2147221164;
539539

540+
internal const int ERROR_INVALID_FILENAME = -2147024773; // Illegal characters in name
540541
internal const int ERROR_ACCESS_DENIED = -2147024891; // ACL'd or r/o
541542
internal const int ERROR_SHARING_VIOLATION = -2147024864; // File locked by another use
542543

0 commit comments

Comments
 (0)