Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/Platform/SharedExtensionHelpers/MergeOutputFileHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,16 @@ private static bool IsDirectoryCaseSensitive(string directory)
{
try
{
string upper = Path.Combine(directory, "CASESENSITIVEPROBE" + Guid.NewGuid().ToString("N"));
using (new FileStream(upper, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 0x1000, FileOptions.DeleteOnClose))
string probeName = "CASESENSITIVEPROBE" + Guid.NewGuid().ToString("N");
string probePath = Path.Combine(directory, probeName);
using (new FileStream(probePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 0x1000, FileOptions.DeleteOnClose))
{
return !File.Exists(upper.ToLowerInvariant());
// Only lower-case the generated probe FILE NAME, keeping the real 'directory' path intact.
// Lower-casing the whole path would corrupt the directory portion, so a case-insensitive
// child directory sitting beneath a case-sensitive parent (e.g. an ext4 casefold dir named
// with uppercase chars) would be probed at a non-existent lowercased parent, File.Exists
// would return false, and the location would be misreported as case-sensitive.
return !File.Exists(Path.Combine(directory, probeName.ToLowerInvariant()));
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,57 @@ await Assert.ThrowsExactlyAsync<ArgumentException>(
}
}

[TestMethod]
public async Task MergeToFileAsync_WhenOutputDiffersFromInputOnlyByCase_IsRejectedOnCaseInsensitiveDirectory()
{
// Regression: the case-sensitivity probe used to lower-case the WHOLE candidate path, corrupting the
// directory portion, so a case-insensitive location (e.g. a Windows temp dir) could be misreported as
// case-sensitive. That made the alias check use an ordinal comparison and miss that 'report.json' and
// 'REPORT.json' are the SAME file, letting the merge overwrite a read-only input. With the probe fixed
// (only the generated file name is lower-cased), a case-only output must alias the input and be
// rejected on a case-insensitive directory; on a genuinely case-sensitive one the names are distinct.
string tempDirectory = Path.Combine(Path.GetTempPath(), $"ctrf-merge-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDirectory);
Comment thread
Evangelink marked this conversation as resolved.
try
{
string input = Path.Combine(tempDirectory, "report.json");
File.WriteAllText(input, BuildReport());

string casedOutput = Path.Combine(tempDirectory, "REPORT.json");
if (IsDirectoryCaseInsensitive(tempDirectory))
{
await Assert.ThrowsExactlyAsync<ArgumentException>(
() => CtrfReportMerger.MergeToFileAsync([input], casedOutput, CancellationToken.None));
Assert.IsTrue(File.Exists(input));
}
else
{
await CtrfReportMerger.MergeToFileAsync([input], casedOutput, CancellationToken.None);
Assert.IsTrue(File.Exists(input));
Assert.IsTrue(File.Exists(casedOutput));
}
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
}
}

private static bool IsDirectoryCaseInsensitive(string directory)
{
string probe = Path.Combine(directory, "CaseProbe" + Guid.NewGuid().ToString("N"));
File.WriteAllText(probe, string.Empty);
try
{
// Only the file name is lower-cased so the (possibly case-sensitive) directory path stays intact.
return File.Exists(Path.Combine(directory, Path.GetFileName(probe).ToLowerInvariant()));
}
finally
{
File.Delete(probe);
}
}

[TestMethod]
public async Task MergeToFileAsync_WritesMergedFileToDisk()
{
Expand Down
Loading