Skip to content

Commit

Permalink
Add support for JRE provisioning: Jre tar.gz unpack
Browse files Browse the repository at this point in the history
  • Loading branch information
gregory-paidis-sonarsource committed Jul 9, 2024
1 parent ffd1271 commit 282751a
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 25 deletions.
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 @@ -44,6 +44,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 @@ -70,7 +71,8 @@ public JreCacheTests()
checksum = Substitute.For<IChecksum>();
unpacker = Substitute.For<IUnpacker>();
unpackerFactory = Substitute.For<IUnpackerFactory>();
unpackerFactory.CreateForArchive(directoryWrapper, fileWrapper, TestArchiveName).Returns(unpacker);
operatingSystemProvider = Substitute.For<IOperatingSystemProvider>();
unpackerFactory.CreateForArchive(directoryWrapper, fileWrapper, operatingSystemProvider, TestArchiveName).Returns(unpacker);
}

[TestMethod]
Expand Down Expand Up @@ -233,7 +235,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(TestArchiveName, sha, "javaPath"), () => Task.FromResult<Stream>(new MemoryStream(downloadContentArray)));
Expand Down Expand Up @@ -265,7 +267,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(TestArchiveName, sha, "javaPath"), () => throw new InvalidOperationException("Download failure simulation."));
Expand Down Expand Up @@ -565,7 +567,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.CreateForArchive(directoryWrapper, fileWrapper, TestArchiveName).Returns(Substitute.For<IUnpacker>());
unpackerFactory.CreateForArchive(directoryWrapper, fileWrapper, operatingSystemProvider, TestArchiveName).Returns(Substitute.For<IUnpacker>());

var sut = CreateSutWithSubstitutes();
var result = await sut.DownloadJreAsync(home, new(TestArchiveName, "sha256", "javaPath"), () => Task.FromResult<Stream>(new MemoryStream()));
Expand All @@ -574,7 +576,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).CreateForArchive(directoryWrapper, fileWrapper, TestArchiveName);
unpackerFactory.Received(1).CreateForArchive(directoryWrapper, fileWrapper, operatingSystemProvider, TestArchiveName);
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 'sha256' and the expected checksum is 'sha256'."),
Expand All @@ -591,7 +593,7 @@ public async Task UnpackerFactory_ReturnsNull()
var sha = Path.Combine(cache, "sha256");
directoryWrapper.Exists(cache).Returns(true);
directoryWrapper.Exists(sha).Returns(true);
unpackerFactory.CreateForArchive(directoryWrapper, fileWrapper, TestArchiveName).ReturnsNull();
unpackerFactory.CreateForArchive(directoryWrapper, fileWrapper, operatingSystemProvider, TestArchiveName).ReturnsNull();

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

Expand All @@ -612,7 +614,7 @@ public async Task UnpackerFactory_Throws()
var sha = Path.Combine(cache, "sha256");
directoryWrapper.Exists(cache).Returns(true);
directoryWrapper.Exists(sha).Returns(true);
unpackerFactory.CreateForArchive(directoryWrapper, fileWrapper, TestArchiveName).ReturnsNull();
unpackerFactory.CreateForArchive(directoryWrapper, fileWrapper, operatingSystemProvider, TestArchiveName).ReturnsNull();

var sut = CreateSutWithSubstitutes();
var result = await sut.DownloadJreAsync(home, new(TestArchiveName, "sha256", "javaPath"), () => Task.FromResult<Stream>(new MemoryStream()));
Expand All @@ -621,7 +623,7 @@ public async Task UnpackerFactory_Throws()
fileWrapper.DidNotReceiveWithAnyArgs().Create(null);
fileWrapper.DidNotReceiveWithAnyArgs().Open(null);
checksum.DidNotReceiveWithAnyArgs().ComputeHash(null);
unpackerFactory.Received(1).CreateForArchive(directoryWrapper, fileWrapper, TestArchiveName);
unpackerFactory.Received(1).CreateForArchive(directoryWrapper, fileWrapper, operatingSystemProvider, TestArchiveName);
}

[TestMethod]
Expand Down Expand Up @@ -814,7 +816,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 @@ -843,5 +845,5 @@ public async Task EndToEndTestWithFiles_Success()
}

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,110 @@
/*
* 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 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
{
[TestMethod]
public void TarGzUnpacking_Success()
{
// A sample zip file 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());
var main = Path.Combine(baseDirectory, "Main");
var sub1 = Path.Combine(baseDirectory, "Main", "Sub");
var sub2 = Path.Combine(baseDirectory, "Main", "Sub2");
var sampleTxt = Path.Combine(baseDirectory, "Main", "Sub2", "Sample.txt");
var osProvider = Substitute.For<IOperatingSystemProvider>();
osProvider.OperatingSystem().Returns(PlatformOS.MacOSX);
using var archive = new MemoryStream(Convert.FromBase64String(sampleTarGzFile));
var sut = new TarGzUnpacker(DirectoryWrapper.Instance, FileWrapper.Instance, osProvider);
try
{
sut.Unpack(archive, baseDirectory);

Directory.Exists(main).Should().BeTrue();
Directory.Exists(sub1).Should().BeTrue();
Directory.Exists(sub2).Should().BeTrue();
File.Exists(sampleTxt).Should().BeTrue();
var content = File.ReadAllText(sampleTxt).NormalizeLineEndings();
content.Should().Be("hey beautiful");
}
finally
{
Directory.Delete(baseDirectory, true);
}
}

[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 = new TarGzUnpacker(DirectoryWrapper.Instance, FileWrapper.Instance, Substitute.For<IOperatingSystemProvider>());

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

action.Should().Throw<Exception>().WithMessage("Error GZIP header, first magic byte doesn't match");
Directory.Exists(baseDirectory).Should().BeFalse();
}

[TestMethod]
public void TarGzUnpacking_ZipSlip_IsDetected()
{
// zip-slip.zip 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 = new TarGzUnpacker(DirectoryWrapper.Instance, FileWrapper.Instance, Substitute.For<IOperatingSystemProvider>());

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

action.Should().Throw<InvalidNameException>().WithMessage("Parent traversal in paths is not allowed");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,28 @@ 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(@"c:\test\File.GZ", typeof(TarGzUnpacker))]
[DataRow(@"/usr/File.TAR.gz", typeof(TarGzUnpacker))]
public void SupportedFileExtensions(string fileName, Type expectedUnpacker)
{
var sut = new UnpackerFactory();
var unpacker = sut.CreateForArchive(Substitute.For<IDirectoryWrapper>(), Substitute.For<IFileWrapper>(), fileName);

var unpacker = sut.CreateForArchive(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")]
public void UnsupportedFileExtensions(string fileName)
{
var sut = new UnpackerFactory();
var unpacker = sut.CreateForArchive(Substitute.For<IDirectoryWrapper>(), Substitute.For<IFileWrapper>(), fileName);

var unpacker = sut.CreateForArchive(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 @@ -24,5 +24,5 @@ namespace SonarScanner.MSBuild.PreProcessor.JreCaching;

public interface IUnpackerFactory
{
IUnpacker CreateForArchive(IDirectoryWrapper directoryWrapper, IFileWrapper fileWrapper, string archive);
IUnpacker CreateForArchive(IDirectoryWrapper directoryWrapper, IFileWrapper fileWrapper, IOperatingSystemProvider operatingSystemProvider, string archive);
}
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.CreateForArchive(directoryWrapper, fileWrapper, jreDescriptor.Filename) is not { } unpacker)
if (unpackerFactory.CreateForArchive(directoryWrapper, fileWrapper, operatingSystemProvider, jreDescriptor.Filename) is not { } unpacker)
{
return new JreCacheFailure(string.Format(Resources.ERR_JreArchiveFormatNotSupported, jreDescriptor.Filename));
}
Expand Down
95 changes: 95 additions & 0 deletions src/SonarScanner.MSBuild.PreProcessor/JreCaching/TarGzUnpacker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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 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 gzipStream = new GZipInputStream(archive);
using var tarIn = new TarInputStream(gzipStream, 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);
// If translation is disabled, just copy the entry across directly.
tar.CopyEntryContents(outputStream);

#if NETSTANDARD
if (operatingSystemProvider.OperatingSystem() is PlatformOS.Linux or PlatformOS.Alpine)
{
_ = new Mono.Unix.UnixFileInfo(destinationFile)
{
FileAccessPermissions = (Mono.Unix.FileAccessPermissions)entry.TarHeader.Mode // set the same permissions as inside the archive
};
}
#endif
}
}
}
Loading

0 comments on commit 282751a

Please sign in to comment.