From e7921857c38994dd1c08823c41a221c6a7ce76b1 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan <gabriela.trutan@sonarsource.com> Date: Tue, 3 Sep 2024 16:29:35 +0200 Subject: [PATCH] SLVS-1436 Escape rfc 3986 reserved chars in file names. --- .../Common/Models/FileUriTests.cs | 24 +++++++++++++++ src/SLCore/Common/Models/FileUri.cs | 29 +++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/SLCore.UnitTests/Common/Models/FileUriTests.cs b/src/SLCore.UnitTests/Common/Models/FileUriTests.cs index b103b37f05..bde788b158 100644 --- a/src/SLCore.UnitTests/Common/Models/FileUriTests.cs +++ b/src/SLCore.UnitTests/Common/Models/FileUriTests.cs @@ -114,6 +114,19 @@ public void ToString_PercentEncodesBackticks() new FileUri(@"C:\filewithbacktick`1").ToString().Should().Be("file:///C:/filewithbacktick%601"); } + [TestMethod] + [DataRow("[", "%5B")] + [DataRow("]", "%5D")] + [DataRow("#", "%2523")] + [DataRow("@", "%40")] + public void ToString_PercentEncodesReservedRfc3986Characters(string reservedChar, string expectedEncoding) + { + var actualString = @$"C:\filewithRfc3986ReservedChar{reservedChar}.cs"; + var expectedString = @$"file:///C:/filewithRfc3986ReservedChar{expectedEncoding}.cs"; + + new FileUri(actualString).ToString().Should().Be(expectedString); + } + [TestMethod] public void LocalPath_ReturnsCorrectPath() { @@ -152,4 +165,15 @@ public void Deserialize_ProducesCorrectUri() fileUri.ToString().Should().Be("file:///C:/file%20with%20%204%20spaces%20and%20a%20back%60tick"); fileUri.LocalPath.Should().Be(@"C:\file with 4 spaces and a back`tick"); } + + [TestMethod] + public void Deserialize_ReservedRfc3986Characters_ProducesCorrectUri() + { + var serialized = @"""file:///C:/file%5B%5Dand%2523and%40"""; + + var fileUri = JsonConvert.DeserializeObject<FileUri>(serialized); + + fileUri.ToString().Should().Be("file:///C:/file%5B%5Dand%2523and%40"); + fileUri.LocalPath.Should().Be(@"C:\file[]and#and@"); + } } diff --git a/src/SLCore/Common/Models/FileUri.cs b/src/SLCore/Common/Models/FileUri.cs index 760eb0f330..775de8ab4c 100644 --- a/src/SLCore/Common/Models/FileUri.cs +++ b/src/SLCore/Common/Models/FileUri.cs @@ -20,6 +20,8 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; using SonarLint.VisualStudio.SLCore.Protocol; namespace SonarLint.VisualStudio.SLCore.Common.Models; @@ -28,15 +30,38 @@ namespace SonarLint.VisualStudio.SLCore.Common.Models; public sealed class FileUri { private readonly Uri uri; + private static readonly char[] Rfc3986ReservedCharsToEncoding = ['?', '#', '[', ']', '@']; public FileUri(string uriString) { - uri = new Uri(uriString); + var unescapedUri = Uri.UnescapeDataString(uriString); + uri = new Uri(unescapedUri); } public string LocalPath => uri.LocalPath; - public override string ToString() => Uri.EscapeUriString(uri.ToString()); + public override string ToString() + { + var escapedUri = Uri.EscapeUriString(uri.ToString()); + + return EscapeRfc3986ReservedCharacters(escapedUri); + } + + /// <summary> + /// The backend (SlCore) uses java, in which the Uri follows the RFC 3986 protocol. + /// The <see cref="Uri.EscapeUriString"/> does not escape the reserved characters, that's why they are escaped here. + /// See https://learn.microsoft.com/en-us/dotnet/api/system.uri.escapeuristring?view=netframework-4.7.2 + /// </summary> + /// <param name="stringToEscape"></param> + /// <returns></returns> + private static string EscapeRfc3986ReservedCharacters(string stringToEscape) + { + var charsToEscape = Rfc3986ReservedCharsToEncoding.Where(stringToEscape.Contains).ToList(); + + return !charsToEscape.Any() + ? stringToEscape + : charsToEscape.Aggregate(stringToEscape, (current, charToEscape) => current.Replace(charToEscape.ToString(), Uri.HexEscape(charToEscape))); + } [ExcludeFromCodeCoverage] public override bool Equals(object obj)