Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JRE provisioning: Jre tar.gz unpack #2036

Merged
merged 12 commits into from
Jul 12, 2024
Merged
3 changes: 2 additions & 1 deletion NuGet.Config
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -231,7 +233,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)));
Expand Down Expand Up @@ -263,7 +265,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."));
Expand Down Expand Up @@ -564,7 +566,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()));
Expand All @@ -573,7 +575,7 @@ public async Task UnpackerFactory_Success()
fileWrapper.Received(1).Create(Arg.Any<string>());
fileWrapper.Received(2).Open(file); // One for the checksum and the other 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'.",
Expand All @@ -590,7 +592,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()));
Expand All @@ -599,7 +601,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();
}

Expand All @@ -611,7 +613,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()));
Expand All @@ -620,7 +622,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]
Expand Down Expand Up @@ -802,7 +804,7 @@ public async Task EndToEndTestWithFiles_Success()
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, realDirectoryWrapper, realFileWrapper, realChecksum, realUnpackerFactory, operatingSystemProvider);
try
{
var result = await sut.DownloadJreAsync(home, jreDescriptor, () => Task.FromResult<Stream>(new MemoryStream(zipContent)));
Expand Down Expand Up @@ -831,5 +833,5 @@ public async Task EndToEndTestWithFiles_Success()
}

private JreCache CreateSutWithSubstitutes() =>
new JreCache(testLogger, directoryWrapper, fileWrapper, checksum, unpackerFactory);
new JreCache(testLogger, directoryWrapper, fileWrapper, checksum, unpackerFactory, operatingSystemProvider);
}
gregory-paidis-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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);
gregory-paidis-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
using var archive = new MemoryStream(Convert.FromBase64String(zipWithRootedPath));

CreateUnpacker().Unpack(archive, 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
Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -35,6 +35,8 @@ public interface IOperatingSystemProvider
{
public PlatformOS OperatingSystem();

public bool IsUnix();
gregory-paidis-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

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

bool DirectoryExists(string path);
Expand Down
5 changes: 5 additions & 0 deletions src/SonarScanner.MSBuild.Common/OperatingSystemProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -25,7 +25,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)
{
Expand Down Expand Up @@ -55,7 +61,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));
}
Expand Down
Loading
Loading