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 03d3f67
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 10 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 @@ -813,7 +813,7 @@ public async Task EndToEndTestWithFiles_Success()
var realDirectoryWrapper = DirectoryWrapper.Instance;
var realFileWrapper = FileWrapper.Instance;
var realChecksum = new ChecksumSha256();
var realUnpackerFactory = new UnpackerFactory();
var realUnpackerFactory = new UnpackerFactory(Substitute.For<IOperatingSystemProvider>());
var sut = new JreCache(testLogger, realDirectoryWrapper, realFileWrapper, realChecksum, realUnpackerFactory);
try
{
Expand Down
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 sut = new UnpackerFactory(Substitute.For<IOperatingSystemProvider>());

var unpacker = sut.CreateForArchive(Substitute.For<IDirectoryWrapper>(), Substitute.For<IFileWrapper>(), 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 sut = new UnpackerFactory(Substitute.For<IOperatingSystemProvider>());

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

unpacker.Should().BeNull();
}
}
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

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

namespace SonarScanner.MSBuild.PreProcessor.JreCaching;

public class UnpackerFactory : IUnpackerFactory
public class UnpackerFactory(IOperatingSystemProvider operatingSystemProvider) : IUnpackerFactory
{
public static UnpackerFactory Instance { get; } = new UnpackerFactory();

public IUnpacker CreateForArchive(IDirectoryWrapper directoryWrapper, IFileWrapper fileWrapper, string archive) =>
Path.GetExtension(archive).ToUpperInvariant() switch
{
".ZIP" => new ZipUnpacker(),
_ => null,
".GZ" => new TarGzUnpacker(directoryWrapper, fileWrapper, operatingSystemProvider),
_ => null
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<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>
Expand Down

0 comments on commit 03d3f67

Please sign in to comment.