{
FileType targetType = Target.TargetSpecificFileType(type);
(long startOffset, long compressedSize) = AddToBundle(bundle, file, targetType);
- FileEntry entry = BundleManifest.AddEntry(targetType, relativePath, startOffset, file.Length, compressedSize, Target.BundleMajorVersion);
+ FileEntry entry = BundleManifest.AddEntry(targetType, file, relativePath, startOffset, compressedSize, Target.BundleMajorVersion);
Tracer.Log($"Embed: {entry}");
}
}
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Security.Cryptography;
namespace Microsoft.NET.HostModel.Bundle
{
// identify this bundle. It is choosen to be compatible
// with path-names so that the AppHost can use it in
// extraction path.
- public readonly string BundleID;
+ public string BundleID { get; private set; }
+ //Same as Path.GetRandomFileName
+ private const int BundleIdLength = 12;
+ private SHA256 bundleHash = SHA256.Create();
public readonly uint BundleMajorVersion;
// The Minor version is currently unused, and is always zero
public const uint BundleMinorVersion = 0;
{
BundleMajorVersion = bundleMajorVersion;
Files = new List<FileEntry>();
- BundleID = Path.GetRandomFileName();
Flags = (netcoreapp3CompatMode) ? HeaderFlags.NetcoreApp3CompatMode : HeaderFlags.None;
}
- public FileEntry AddEntry(FileType type, string relativePath, long offset, long size, long compressedSize, uint bundleMajorVersion)
+ public FileEntry AddEntry(FileType type, FileStream fileContent, string relativePath, long offset, long compressedSize, uint bundleMajorVersion)
{
- FileEntry entry = new FileEntry(type, relativePath, offset, size, compressedSize, bundleMajorVersion);
+ if (bundleHash == null)
+ {
+ throw new InvalidOperationException("It is forbidden to change Manifest state after it was written or BundleId was obtained.");
+ }
+
+ FileEntry entry = new FileEntry(type, relativePath, offset, fileContent.Length, compressedSize, bundleMajorVersion);
Files.Add(entry);
+ fileContent.Position = 0;
+ byte[] hashBytes = ComputeSha256Hash(fileContent);
+ bundleHash.TransformBlock(hashBytes, 0, hashBytes.Length, hashBytes, 0);
+
switch (entry.Type)
{
case FileType.DepsJson:
return entry;
}
+ private static byte[] ComputeSha256Hash(Stream stream)
+ {
+ using (SHA256 sha = SHA256.Create())
+ {
+ return sha.ComputeHash(stream);
+ }
+ }
+
+ private string GenerateDeterministicId()
+ {
+ bundleHash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
+ byte[] manifestHash = bundleHash.Hash;
+ bundleHash.Dispose();
+ bundleHash = null;
+
+ return Convert.ToBase64String(manifestHash).Substring(BundleIdLength).Replace('/', '_');
+ }
+
public long Write(BinaryWriter writer)
{
+ BundleID = BundleID ?? GenerateDeterministicId();
+
long startOffset = writer.BaseStream.Position;
// Write the bundle header
bundler.BundleManifest.Files.Where(entry => entry.RelativePath.Equals("rel/app.Repeat.dll")).Single().Type.Should().Be(FileType.Assembly);
}
+ private (string bundleFileName, string bundleId) CreateSampleBundle(bool bundleMultipleFiles)
+ {
+ var fixture = sharedTestState.TestFixture.Copy();
+
+ var hostName = BundleHelper.GetHostName(fixture);
+ var bundleDir = Directory.CreateDirectory(
+ Path.Combine(BundleHelper.GetBundleDir(fixture).FullName, Path.GetRandomFileName()));
+ var targetOS = BundleHelper.GetTargetOS(fixture.CurrentRid);
+ var targetArch = BundleHelper.GetTargetArch(fixture.CurrentRid);
+
+ var fileSpecs = new List<FileSpec>();
+ fileSpecs.Add(new FileSpec(BundleHelper.GetHostPath(fixture), BundleHelper.GetHostName(fixture)));
+ if (bundleMultipleFiles)
+ {
+ fileSpecs.Add(new FileSpec(BundleHelper.GetAppPath(fixture), "rel/app.repeat.dll"));
+ }
+
+ Bundler bundler = new Bundler(hostName, bundleDir.FullName, targetOS: targetOS, targetArch: targetArch);
+ return (bundler.GenerateBundle(fileSpecs), bundler.BundleManifest.BundleID);
+ }
+
+ [Fact]
+ public void TestWithIdenticalBundlesShouldBeBinaryEqualPasses()
+ {
+ var firstBundle = CreateSampleBundle(true);
+ byte[] firstBundleContent = File.ReadAllBytes(firstBundle.bundleFileName);
+ var secondBundle = CreateSampleBundle(true);
+ byte[] secondBundleContent = File.ReadAllBytes(secondBundle.bundleFileName);
+
+ firstBundle.bundleId.ShouldBeEquivalentTo(secondBundle.bundleId,
+ "Deterministic/Reproducible build should produce identical bundle id for identical inputs");
+ firstBundleContent.ShouldBeEquivalentTo(secondBundleContent,
+ "Deterministic/Reproducible build should produce identical binary for identical inputs");
+ }
+
+ [Fact]
+ public void TestWithUniqueBundlesShouldHaveUniqueBundleIdsPasses()
+ {
+ string firstBundle = CreateSampleBundle(true).bundleId;
+ string secondBundle = CreateSampleBundle(false).bundleId;
+
+ Assert.NotEqual(firstBundle, secondBundle, StringComparer.Ordinal);
+ }
+
[Fact]
public void TestWithMultipleDuplicateEntriesFails()
{