Skip to content

Commit

Permalink
Add support for JRE provisioning: Jre tar.gz unpack (#2036)
Browse files Browse the repository at this point in the history
gregory-paidis-sonarsource authored Jul 12, 2024
1 parent 72a7718 commit 3c6df25
Showing 14 changed files with 370 additions and 33 deletions.
3 changes: 2 additions & 1 deletion NuGet.Config
Original file line number Diff line number Diff line change
@@ -32,7 +32,8 @@
<!-- grpc-packages = Grpc.Tools (gRPC and Protocol Buffer compiler) -->
<!-- protobuf-packages = Google.Protobuf -->
<!-- Nsubstitute = author of NSubstitute -->
<owners>Microsoft;sharwell;meirb;dotnetfoundation;castleproject;jonorossi;onovotny;fluentassertions;jamesnk;CycloneDX;grpc-packages;protobuf-packages;NSubstitute;kzu</owners>
<!-- SharpDevelop = author of SharpZipLib -->
<owners>Microsoft;sharwell;meirb;dotnetfoundation;castleproject;jonorossi;onovotny;fluentassertions;jamesnk;CycloneDX;grpc-packages;protobuf-packages;NSubstitute;kzu;SharpDevelop</owners>
</repository>
<author name="Microsoft">
<!-- Subject Name: CN=Microsoft Corporation, valid from 2023-07-27 -->
Original file line number Diff line number Diff line change
@@ -293,8 +293,12 @@ private sealed class OperatingSystemProvider(
Func<string, bool> directoryExistsFunc) : IOperatingSystemProvider
{
public PlatformOS OperatingSystem() => os;

public bool DirectoryExists(string path) => directoryExistsFunc(path);

public string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option) => pathFunc(folder, option);

public bool IsUnix() => throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ public class JreCacheTests
private readonly IChecksum checksum;
private readonly IUnpacker unpacker;
private readonly IUnpackerFactory unpackerFactory;
private readonly IOperatingSystemProvider operatingSystemProvider;

// https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.createdirectory
// https://learn.microsoft.com/en-us/dotnet/api/system.io.file.create
@@ -68,7 +69,8 @@ public JreCacheTests()
checksum = Substitute.For<IChecksum>();
unpacker = Substitute.For<IUnpacker>();
unpackerFactory = Substitute.For<IUnpackerFactory>();
unpackerFactory.Create(directoryWrapper, fileWrapper, "filename.tar.gz").Returns(unpacker);
operatingSystemProvider = Substitute.For<IOperatingSystemProvider>();
unpackerFactory.Create(directoryWrapper, fileWrapper, operatingSystemProvider, "filename.tar.gz").Returns(unpacker);
}

[TestMethod]
@@ -261,7 +263,7 @@ public async Task Download_DownloadFileNew_Success_WithTestFiles()
var fileWrapperIO = FileWrapper.Instance;
var downloadContentArray = new byte[] { 1, 2, 3 };

var sut = new JreCache(testLogger, directoryWrapperIO, fileWrapperIO, checksum, unpackerFactory);
var sut = new JreCache(testLogger, directoryWrapperIO, fileWrapperIO, checksum, unpackerFactory, operatingSystemProvider);
try
{
var result = await sut.DownloadJreAsync(home, new("filename.tar.gz", sha, "javaPath"), () => Task.FromResult<Stream>(new MemoryStream(downloadContentArray)));
@@ -294,7 +296,7 @@ public async Task Download_DownloadFileNew_Failure_WithTestFiles()
var directoryWrapperIO = DirectoryWrapper.Instance; // Do real I/O operations in this test and only fake the download.
var fileWrapperIO = FileWrapper.Instance;

var sut = new JreCache(testLogger, directoryWrapperIO, fileWrapperIO, checksum, unpackerFactory);
var sut = new JreCache(testLogger, directoryWrapperIO, fileWrapperIO, checksum, unpackerFactory, operatingSystemProvider);
try
{
var result = await sut.DownloadJreAsync(home, new("filename.tar.gz", sha, "javaPath"), () => throw new InvalidOperationException("Download failure simulation."));
@@ -612,7 +614,7 @@ public async Task UnpackerFactory_Success()
fileWrapper.Exists(file).Returns(false);
fileWrapper.Create(Arg.Any<string>()).Returns(new MemoryStream());
checksum.ComputeHash(Arg.Any<Stream>()).Returns("sha256");
unpackerFactory.Create(directoryWrapper, fileWrapper, "filename.tar.gz").Returns(Substitute.For<IUnpacker>());
unpackerFactory.Create(directoryWrapper, fileWrapper, operatingSystemProvider, "filename.tar.gz").Returns(Substitute.For<IUnpacker>());

var sut = CreateSutWithSubstitutes();
var result = await sut.DownloadJreAsync(home, new("filename.tar.gz", "sha256", "javaPath"), () => Task.FromResult<Stream>(new MemoryStream()));
@@ -621,7 +623,7 @@ public async Task UnpackerFactory_Success()
fileWrapper.Received(1).Create(Arg.Any<string>());
fileWrapper.Received(1).Open(file); // For the unpacking.
checksum.Received(1).ComputeHash(Arg.Any<Stream>());
unpackerFactory.Received(1).Create(directoryWrapper, fileWrapper, "filename.tar.gz");
unpackerFactory.Received(1).Create(directoryWrapper, fileWrapper, operatingSystemProvider, "filename.tar.gz");
testLogger.DebugMessages.Should().BeEquivalentTo(
@"Starting the Java Runtime Environment download.",
@"The checksum of the downloaded file is 'sha256' and the expected checksum is 'sha256'.",
@@ -638,7 +640,7 @@ public async Task UnpackerFactory_ReturnsNull()
var sha = Path.Combine(cache, "sha256");
directoryWrapper.Exists(cache).Returns(true);
directoryWrapper.Exists(sha).Returns(true);
unpackerFactory.Create(directoryWrapper, fileWrapper, "filename.tar.gz").ReturnsNull();
unpackerFactory.Create(directoryWrapper, fileWrapper, operatingSystemProvider, "filename.tar.gz").ReturnsNull();

var sut = CreateSutWithSubstitutes();
var result = await sut.DownloadJreAsync(home, new("filename.tar.gz", "sha256", "javaPath"), () => Task.FromResult<Stream>(new MemoryStream()));
@@ -647,7 +649,7 @@ public async Task UnpackerFactory_ReturnsNull()
fileWrapper.DidNotReceiveWithAnyArgs().Create(null);
fileWrapper.DidNotReceiveWithAnyArgs().Open(null);
checksum.DidNotReceiveWithAnyArgs().ComputeHash(null);
unpackerFactory.Received(1).Create(directoryWrapper, fileWrapper, "filename.tar.gz");
unpackerFactory.Received(1).Create(directoryWrapper, fileWrapper, operatingSystemProvider, "filename.tar.gz");
testLogger.DebugMessages.Should().BeEmpty();
}

@@ -659,7 +661,7 @@ public async Task UnpackerFactory_UnsupportedFormat()
var sha = Path.Combine(cache, "sha256");
directoryWrapper.Exists(cache).Returns(true);
directoryWrapper.Exists(sha).Returns(true);
unpackerFactory.Create(directoryWrapper, fileWrapper, "filename.tar.gz").ReturnsNull();
unpackerFactory.Create(directoryWrapper, fileWrapper, operatingSystemProvider, "filename.tar.gz").ReturnsNull();

var sut = CreateSutWithSubstitutes();
var result = await sut.DownloadJreAsync(home, new("filename.tar.gz", "sha256", "javaPath"), () => Task.FromResult<Stream>(new MemoryStream()));
@@ -668,7 +670,7 @@ public async Task UnpackerFactory_UnsupportedFormat()
fileWrapper.DidNotReceiveWithAnyArgs().Create(null);
fileWrapper.DidNotReceiveWithAnyArgs().Open(null);
checksum.DidNotReceiveWithAnyArgs().ComputeHash(null);
unpackerFactory.Received(1).Create(directoryWrapper, fileWrapper, "filename.tar.gz");
unpackerFactory.Received(1).Create(directoryWrapper, fileWrapper, operatingSystemProvider, "filename.tar.gz");
}

[TestMethod]
@@ -831,7 +833,7 @@ public async Task Unpack_Failure_ErrorInCleanUpOfTempDirectory()
}

[TestMethod]
public async Task EndToEndTestWithFiles_Success()
public async Task EndToEndTestWithFiles_Zip_Success()
{
// A zip file with a file named java.exe in the jdk-17.0.11+9-jre\bin folder.
const string jreZip = """
@@ -852,11 +854,8 @@ public async Task EndToEndTestWithFiles_Success()
var sha = "b192f77aa6a6154f788ab74a839b1930d59eb1034c3fe617ef0451466a8335ba";
var file = "OpenJDK17U-jre_x64_windows_hotspot_17.0.11_9.zip";
var jreDescriptor = new JreDescriptor(file, sha, @"jdk-17.0.11+9-jre/bin/java.exe");
var realDirectoryWrapper = DirectoryWrapper.Instance;
var realFileWrapper = FileWrapper.Instance;
var realChecksum = new ChecksumSha256();
var realUnpackerFactory = new UnpackerFactory();
var sut = new JreCache(testLogger, realDirectoryWrapper, realFileWrapper, realChecksum, realUnpackerFactory);
var sut = new JreCache(testLogger, DirectoryWrapper.Instance, FileWrapper.Instance, new ChecksumSha256(), UnpackerFactory.Instance, operatingSystemProvider);

try
{
var result = await sut.DownloadJreAsync(home, jreDescriptor, () => Task.FromResult<Stream>(new MemoryStream(zipContent)));
@@ -884,6 +883,52 @@ public async Task EndToEndTestWithFiles_Success()
}
}

[TestMethod]
public async Task EndToEndTestWithFiles_TarGz_Success()
{
// A tarball with a file named java.exe in the jdk-17.0.11+9-jre\bin folder.
const string jreTarBall = """
H4sICLHekGYEAGpkay0xNy4wLjExKzktanJlLnRhcgDt0kEKAiEUgGGP8vbR5HM06
R5dwBhrtMkJtej4OUFQFEQgbsYPUReu/J/tjkuUDW0QF5ul9XpFsqOJlDKdD2/n84
58zTkKyRAJRcoEIyBIAZcQlQcgM/XZf2dc5hn4qz+b+qcBaGv/Er73t+qqGn3TJIt
f/fG1f5veIeOCEqCkgJn33/YmQFo2/QMoCOp0HjTszbSNHqIO0bgDKNeBGyPEXoPX
aoC8E1JVVVWVdgcIF31QAAwAAA==
""";
var tarContent = Convert.FromBase64String(jreTarBall);
var home = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
var cache = Path.Combine(home, "cache");
var sha = "347f62ce8b0aadffd19736a189b4b79fad87a83cc36ec1273081629c9cb06d3b";
var file = "OpenJDK17U-jre_x64_windows_hotspot_17.0.11_9.tar.gz";
var jreDescriptor = new JreDescriptor(file, sha, @"jdk-17.0.11+9-jre/bin/java.exe");
var sut = new JreCache(testLogger, DirectoryWrapper.Instance, FileWrapper.Instance, new ChecksumSha256(), UnpackerFactory.Instance, operatingSystemProvider);
try
{
var result = await sut.DownloadJreAsync(home, jreDescriptor, () => Task.FromResult<Stream>(new MemoryStream(tarContent)));

result.Should().BeOfType<JreCacheHit>().Which.JavaExe.Should().Be(
$@"{home}\cache\347f62ce8b0aadffd19736a189b4b79fad87a83cc36ec1273081629c9cb06d3b\OpenJDK17U-jre_x64_windows_hotspot_17.0.11_9.tar.gz_extracted\jdk-17.0.11+9-jre/bin/java.exe");
Directory.EnumerateFileSystemEntries(cache, "*", SearchOption.AllDirectories).Should().BeEquivalentTo(
Path.Combine(cache, sha),
Path.Combine(cache, sha, file),
Path.Combine(cache, sha, $"{file}_extracted"),
Path.Combine(cache, sha, $"{file}_extracted", "jdk-17.0.11+9-jre"),
Path.Combine(cache, sha, $"{file}_extracted", "jdk-17.0.11+9-jre", "bin"),
Path.Combine(cache, sha, $"{file}_extracted", "jdk-17.0.11+9-jre", "bin", "java.exe"));
File.ReadAllText(Path.Combine(cache, sha, $"{file}_extracted", "jdk-17.0.11+9-jre", "bin", "java.exe")).Should().Be(
"This is just a sample file for testing and not the real java.exe");
testLogger.DebugMessages.Should().SatisfyRespectively(
x => x.Should().Be(@$"Starting the Java Runtime Environment download."),
x => x.Should().Be(@$"The checksum of the downloaded file is '347f62ce8b0aadffd19736a189b4b79fad87a83cc36ec1273081629c9cb06d3b' and the expected checksum is '347f62ce8b0aadffd19736a189b4b79fad87a83cc36ec1273081629c9cb06d3b'."),
x => x.Should().Match(@$"Starting extracting the Java runtime environment from archive '{home}\cache\347f62ce8b0aadffd19736a189b4b79fad87a83cc36ec1273081629c9cb06d3b\OpenJDK17U-jre_x64_windows_hotspot_17.0.11_9.tar.gz' to folder '{home}\cache\347f62ce8b0aadffd19736a189b4b79fad87a83cc36ec1273081629c9cb06d3b\*'."),
x => x.Should().Match(@$"Moving extracted Java runtime environment from '{home}\cache\347f62ce8b0aadffd19736a189b4b79fad87a83cc36ec1273081629c9cb06d3b\*' to '{home}\cache\347f62ce8b0aadffd19736a189b4b79fad87a83cc36ec1273081629c9cb06d3b\OpenJDK17U-jre_x64_windows_hotspot_17.0.11_9.tar.gz_extracted'."),
x => x.Should().Be(@$"The Java runtime environment was successfully added to '{home}\cache\347f62ce8b0aadffd19736a189b4b79fad87a83cc36ec1273081629c9cb06d3b\OpenJDK17U-jre_x64_windows_hotspot_17.0.11_9.tar.gz_extracted'."));
}
finally
{
Directory.Delete(home, true);
}
}

private JreCache CreateSutWithSubstitutes() =>
new JreCache(testLogger, directoryWrapper, fileWrapper, checksum, unpackerFactory);
new JreCache(testLogger, directoryWrapper, fileWrapper, checksum, unpackerFactory, operatingSystemProvider);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* SonarScanner for .NET
* Copyright (C) 2016-2024 SonarSource SA
* mailto: info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
using System.IO;
using System.Text;
using FluentAssertions;
using ICSharpCode.SharpZipLib.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using SonarScanner.MSBuild.Common;
using SonarScanner.MSBuild.PreProcessor.JreCaching;
using TestUtilities;

namespace SonarScanner.MSBuild.PreProcessor.Test.JreCaching;

[TestClass]
public class TarGzUnpackTests
{
private readonly IFileWrapper fileWrapper = Substitute.For<IFileWrapper>();
private readonly IDirectoryWrapper directoryWrapper = Substitute.For<IDirectoryWrapper>();
private readonly IOperatingSystemProvider osProvider = Substitute.For<IOperatingSystemProvider>();

[TestMethod]
public void TarGzUnpacking_Success()
{
// A tarball with the following content:
// Main
// ├── Sub
// └── Sub2
// └── Sample.txt
const string sampleTarGzFile = """
H4sICL04jWYEAE1haW4udGFyAO3SUQrDIAyA4RzFE2wao55iTz2BBccK3Ribw
nb7iVDKnkqh+mK+l4S8/rn46XGGumTmnMuz+JvLrsiSRk1ImO/WkgRhoIH0jv
4lBHSq9B/SWPMHdvVHk+9OO+T+LSz9seID7OpPpT8Zy/1bWPsP/v6cwyl+Ihx
ss78ya3+T70qR1iAkNNB5/1v4ijH4FKdrmoExxlgvfmqGu7oADgAA
""";
var baseDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
using var archive = new MemoryStream(Convert.FromBase64String(sampleTarGzFile));
using var unzipped = new MemoryStream();
fileWrapper.Create($"""{baseDirectory}\Main\Sub2\Sample.txt""").Returns(unzipped);

CreateUnpacker().Unpack(archive, baseDirectory);

directoryWrapper.Received(1).CreateDirectory($"""{baseDirectory}\Main\""");
directoryWrapper.Received(1).CreateDirectory($"""{baseDirectory}\Main\Sub\""");
directoryWrapper.Received(1).CreateDirectory($"""{baseDirectory}\Main\Sub2\""");
Encoding.UTF8.GetString(unzipped.ToArray()).NormalizeLineEndings().Should().Be("hey beautiful");
}

[TestMethod]
public void TarGzUnpacking_RootedPath_Success()
{
// A tarball with a single file with a rooted path: "\ sample.txt"
const string zipWithRootedPath = """
H4sIAAAAAAAAA+3OMQ7CMBBE0T3KngCtsY0PwDVoUlghkiEoNhLHB
5QmFdBEEdJ/zRQzxZy0dpdbybv2aLISe0kpvdOlaMucuSAuHIKPto
/eizmXfBS1tQ4t3WvrJlXpp9x/2n3r/9Q5lzLqcaxtuG79BQAAAAA
AAAAAAAAAAADwuyfh1ptHACgAAA==
""";
var baseDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
using var unzipped = new MemoryStream();
fileWrapper.Create($"""{baseDirectory}\ sample.txt""").Returns(unzipped);
using var archive = new MemoryStream(Convert.FromBase64String(zipWithRootedPath));

CreateUnpacker().Unpack(archive, baseDirectory);

directoryWrapper.Received(1).CreateDirectory(baseDirectory);
Encoding.UTF8.GetString(unzipped.ToArray()).NormalizeLineEndings().Should().Be("hello Costin");
}

[TestMethod]
public void TarGzUnpacking_Fails_InvalidZipFile()
{
var baseDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
using var archive = new MemoryStream([1, 2, 3]); // Invalid archive content
var sut = CreateUnpacker();

var action = () => sut.Unpack(archive, baseDirectory);

action.Should().Throw<Exception>().WithMessage("Error GZIP header, first magic byte doesn't match");
directoryWrapper.Received(0).CreateDirectory(Arg.Any<string>());
fileWrapper.Received(0).Create(Arg.Any<string>());
}

[TestMethod]
public void TarGzUnpacking_ZipSlip_IsDetected()
{
// slip.tar.gz from https://github.com/kevva/decompress/issues/71
// google "Zip Slip Vulnerability" for details
const string zipSlip = """
H4sICJDill0C/215LXNsaXAudGFyAO3TvQrCMBSG4cxeRa4gTdKk
XRUULHQo2MlNUET8K7aC9OrFFsTFn0ELlffhwDmcZEngU4EKhunx
sE43h634Dd161rWL3X1u9sZYa4VMRQfOZbU4Sfn1R/aEUgH1YVX7
Iih3m6JYLVV1qcQ/6OLnbnmIoibjJvb6sbesESb0znsfGh8Kba1z
XkjdZf6Pdb1bvbj37ryn+Z8nmcyno1zO0iTLJuOBAAAAAAAAAAAA
AAAAQJ9cAZCup/MAKAAA
""";
var baseDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
using var zipStream = new MemoryStream(Convert.FromBase64String(zipSlip));
var sut = CreateUnpacker();

var action = () => sut.Unpack(zipStream, baseDirectory);

action.Should().Throw<InvalidNameException>().WithMessage("Parent traversal in paths is not allowed");
}

private TarGzUnpacker CreateUnpacker() =>
new(directoryWrapper, fileWrapper, osProvider);
}
Original file line number Diff line number Diff line change
@@ -35,23 +35,30 @@ public class UnpackerFactoryTests
[DataRow("File.ZIP", typeof(ZipUnpacker))]
[DataRow(@"c:\test\File.ZIP", typeof(ZipUnpacker))]
[DataRow(@"/usr/File.zip", typeof(ZipUnpacker))]
[DataRow("File.tar.gz", typeof(TarGzUnpacker))]
[DataRow("File.TAR.GZ", typeof(TarGzUnpacker))]
[DataRow(@"/usr/File.TAR.gz", typeof(TarGzUnpacker))]
[DataRow(@"/usr/File.tar.GZ", typeof(TarGzUnpacker))]
public void SupportedFileExtensions(string fileName, Type expectedUnpacker)
{
var sut = new UnpackerFactory();
var unpacker = sut.Create(Substitute.For<IDirectoryWrapper>(), Substitute.For<IFileWrapper>(), fileName);

var unpacker = sut.Create(Substitute.For<IDirectoryWrapper>(), Substitute.For<IFileWrapper>(), Substitute.For<IOperatingSystemProvider>(), fileName);

unpacker.Should().BeOfType(expectedUnpacker);
}

[DataTestMethod]
[DataRow("File.tar")]
[DataRow("File.tar.gz")]
[DataRow("File.gz")]
[DataRow("File.rar")]
[DataRow("File.7z")]
[DataRow("File.gz")]
[DataRow("File.tar")]
public void UnsupportedFileExtensions(string fileName)
{
var sut = new UnpackerFactory();
var unpacker = sut.Create(Substitute.For<IDirectoryWrapper>(), Substitute.For<IFileWrapper>(), fileName);

var unpacker = sut.Create(Substitute.For<IDirectoryWrapper>(), Substitute.For<IFileWrapper>(), Substitute.For<IOperatingSystemProvider>(), fileName);

unpacker.Should().BeNull();
}
}
Original file line number Diff line number Diff line change
@@ -436,5 +436,7 @@ private sealed class UnixTestOperatingSystemProvider : IOperatingSystemProvider
public string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option) => throw new NotSupportedException();

public bool DirectoryExists(string path) => throw new NotSupportedException();

public bool IsUnix() => throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -35,6 +35,9 @@ public interface IOperatingSystemProvider
{
public PlatformOS OperatingSystem();

/// <summary>Returns <see langword="true" /> for all *nix-like operating systems, including Linux, Alpine, and MacOS.</summary>
public bool IsUnix();

string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option);

bool DirectoryExists(string path);
5 changes: 5 additions & 0 deletions src/SonarScanner.MSBuild.Common/OperatingSystemProvider.cs
Original file line number Diff line number Diff line change
@@ -47,6 +47,11 @@ public bool IsAlpine() =>
IsAlpineRelease("/etc/os-release")
|| IsAlpineRelease("/usr/lib/os-release");

// Not stable testable
[ExcludeFromCodeCoverage]
public bool IsUnix() =>
OperatingSystem() is PlatformOS.Linux or PlatformOS.Alpine or PlatformOS.MacOSX;

// Not stable testable, manual testing was done by running the scanner on Windows, Mac OS X and Linux.
[ExcludeFromCodeCoverage]
private PlatformOS OperatingSystemCore()
Original file line number Diff line number Diff line change
@@ -24,5 +24,5 @@ namespace SonarScanner.MSBuild.PreProcessor.JreCaching;

public interface IUnpackerFactory
{
IUnpacker Create(IDirectoryWrapper directoryWrapper, IFileWrapper fileWrapper, string archive);
IUnpacker Create(IDirectoryWrapper directoryWrapper, IFileWrapper fileWrapper, IOperatingSystemProvider operatingSystemProvider, string archivePath);
}
10 changes: 8 additions & 2 deletions src/SonarScanner.MSBuild.PreProcessor/JreCaching/JreCache.cs
Original file line number Diff line number Diff line change
@@ -26,7 +26,13 @@

namespace SonarScanner.MSBuild.PreProcessor.JreCaching;

internal class JreCache(ILogger logger, IDirectoryWrapper directoryWrapper, IFileWrapper fileWrapper, IChecksum checksum, IUnpackerFactory unpackerFactory) : IJreCache
internal class JreCache(
ILogger logger,
IDirectoryWrapper directoryWrapper,
IFileWrapper fileWrapper,
IChecksum checksum,
IUnpackerFactory unpackerFactory,
IOperatingSystemProvider operatingSystemProvider) : IJreCache
{
public JreCacheResult IsJreCached(string sonarUserHome, JreDescriptor jreDescriptor)
{
@@ -56,7 +62,7 @@ public async Task<JreCacheResult> DownloadJreAsync(string sonarUserHome, JreDesc
return new JreCacheFailure(string.Format(Resources.ERR_CacheDirectoryCouldNotBeCreated, JreRootPath(jreDescriptor, JresCacheRoot(sonarUserHome))));
}
// If we do not support the archive format, there is no point in downloading. Therefore we bail out early in such a case.
if (unpackerFactory.Create(directoryWrapper, fileWrapper, jreDescriptor.Filename) is not { } unpacker)
if (unpackerFactory.Create(directoryWrapper, fileWrapper, operatingSystemProvider, jreDescriptor.Filename) is not { } unpacker)
{
return new JreCacheFailure(string.Format(Resources.ERR_JreArchiveFormatNotSupported, jreDescriptor.Filename));
}
130 changes: 130 additions & 0 deletions src/SonarScanner.MSBuild.PreProcessor/JreCaching/TarGzUnpacker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* SonarScanner for .NET
* Copyright (C) 2016-2024 SonarSource SA
* mailto: info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using SonarScanner.MSBuild.Common;

namespace SonarScanner.MSBuild.PreProcessor.JreCaching;

public class TarGzUnpacker(IDirectoryWrapper directoryWrapper, IFileWrapper fileWrapper, IOperatingSystemProvider operatingSystemProvider) : IUnpacker
{
// ref https://github.com/icsharpcode/SharpZipLib/blob/ff2d7c30bdb2474d507f001bc555405e9f02a0bb/src/ICSharpCode.SharpZipLib/Tar/TarArchive.cs#L608
public void Unpack(Stream archive, string destinationDirectory)
{
using var gzip = new GZipInputStream(archive);
using var tarIn = new TarInputStream(gzip, null);

var destinationFullPath = Path.GetFullPath(destinationDirectory).TrimEnd('/', '\\');
while (tarIn.GetNextEntry() is { } entry)
{
if (entry.TarHeader.TypeFlag is not (TarHeader.LF_LINK or TarHeader.LF_SYMLINK))
{
ExtractEntry(tarIn, destinationFullPath, entry);
}
}
}

// ref https://github.com/icsharpcode/SharpZipLib/blob/ff2d7c30bdb2474d507f001bc555405e9f02a0bb/src/ICSharpCode.SharpZipLib/Tar/TarArchive.cs#L644
private void ExtractEntry(TarInputStream tar, string destinationFullPath, TarEntry entry)
{
var name = entry.Name;
if (Path.IsPathRooted(name))
{
// NOTE:
// for UNC names... \\machine\share\zoom\beet.txt gives \zoom\beet.txt
name = name.Substring(Path.GetPathRoot(name).Length);
}

name = name.Replace('/', Path.DirectorySeparatorChar);
var destinationFile = Path.Combine(destinationFullPath, name);
var destinationFileDirectory = Path.GetDirectoryName(Path.GetFullPath(destinationFile)) ?? string.Empty;
var isRootDir = entry.IsDirectory && entry.Name == string.Empty;

if (!isRootDir && !destinationFileDirectory.StartsWith(destinationFullPath, StringComparison.InvariantCultureIgnoreCase))
{
throw new InvalidNameException("Parent traversal in paths is not allowed");
}

if (entry.IsDirectory)
{
directoryWrapper.CreateDirectory(destinationFile);
}
else
{
directoryWrapper.CreateDirectory(destinationFileDirectory);
using var outputStream = fileWrapper.Create(destinationFile);
tar.CopyEntryContents(outputStream);
outputStream.Close();
try
{
SetPermissions(operatingSystemProvider, entry, destinationFile);
}
catch (Exception ex) // TODO: Test this when SetPermissions is extracted
{
// TODO: Add some verbose logging and inject ILogger
}
}

// TODO: Move this into an IFilePermissionProvider
[ExcludeFromCodeCoverage]
static void SetPermissions(IOperatingSystemProvider operatingSystemProvider, TarEntry source, string destination)
{
if (operatingSystemProvider.IsUnix())
{
if (operatingSystemProvider.OperatingSystem() is PlatformOS.Alpine)
{
// https://github.com/Jackett/Jackett/blob/master/src/Jackett.Server/Services/FilePermissionService.cs#L27
var process = new Process
{
StartInfo = new ProcessStartInfo
{
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "chmod",
Arguments = $"""{Convert.ToString(source.TarHeader.Mode, 8)} "{destination}" """,
}
};
process.Start();
var stdError = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
throw new InvalidOperationException(stdError);
}
}
else
{
_ = new Mono.Unix.UnixFileInfo(destination)
{
FileAccessPermissions = (Mono.Unix.FileAccessPermissions)source.TarHeader.Mode // set the same permissions as inside the archive
};
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System.IO;
using System;
using SonarScanner.MSBuild.Common;

namespace SonarScanner.MSBuild.PreProcessor.JreCaching;
@@ -27,10 +27,11 @@ public class UnpackerFactory : IUnpackerFactory
{
public static UnpackerFactory Instance { get; } = new UnpackerFactory();

public IUnpacker Create(IDirectoryWrapper directoryWrapper, IFileWrapper fileWrapper, string archive) =>
Path.GetExtension(archive).ToUpperInvariant() switch
public IUnpacker Create(IDirectoryWrapper directoryWrapper, IFileWrapper fileWrapper, IOperatingSystemProvider operatingSystemProvider, string archivePath) =>
archivePath switch
{
".ZIP" => new ZipUnpacker(),
_ => null,
_ when archivePath.EndsWith(".ZIP", StringComparison.OrdinalIgnoreCase) => new ZipUnpacker(),
_ when archivePath.EndsWith(".TAR.GZ", StringComparison.OrdinalIgnoreCase) => new TarGzUnpacker(directoryWrapper, fileWrapper, operatingSystemProvider),
_ => null
};
}
Original file line number Diff line number Diff line change
@@ -87,8 +87,12 @@ public IAnalyzerProvider CreateRoslynAnalyzerProvider(ISonarWebServer server, st
return new RoslynAnalyzerProvider(new EmbeddedAnalyzerInstaller(server, localCacheTempPath, logger), logger);
}

public IJreResolver CreateJreResolver(ISonarWebServer server) =>
new JreResolver(server, new JreCache(logger, DirectoryWrapper.Instance, FileWrapper.Instance, ChecksumSha256.Instance, UnpackerFactory.Instance), logger);
public IJreResolver CreateJreResolver(ISonarWebServer server)
{
var osProvider = new OperatingSystemProvider(FileWrapper.Instance, logger);
var cache = new JreCache(logger, DirectoryWrapper.Instance, FileWrapper.Instance, ChecksumSha256.Instance, UnpackerFactory.Instance, osProvider);
return new JreResolver(server, cache, logger);
}

private bool ValidateServerUrl(string serverUrl)
{
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ScannerNetStandardVersion);$(ScannerNetFxVersion)</TargetFrameworks>
<AssemblyName>SonarScanner.MSBuild.PreProcessor</AssemblyName>
@@ -18,7 +18,9 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Mono.Posix" Version="7.1.0-final.1.21458.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>
<ItemGroup>

0 comments on commit 3c6df25

Please sign in to comment.