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)