File: Bundle\Manifest.cs
Web Access
Project: src\src\runtime\src\installer\managed\Microsoft.NET.HostModel\Microsoft.NET.HostModel.csproj (Microsoft.NET.HostModel)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
#if NET
using Base64Url = System.Buffers.Text.Base64Url;
#else
using Base64Url = Microsoft.NET.HostModel.Base64Url;
#endif

namespace Microsoft.NET.HostModel.Bundle
{
    /// <summary>
    ///  BundleManifest is a description of the contents of a bundle file.
    ///  This class handles creation and consumption of bundle-manifests.
    ///
    ///  Here is the description of the Bundle Layout:
    ///  _______________________________________________
    ///  AppHost
    ///
    ///
    /// ------------Embedded Files ---------------------
    /// The embedded files including the app, its
    /// configuration files, dependencies, and
    /// possibly the runtime.
    ///
    ///
    ///
    ///
    ///
    ///
    ///
    /// ------------ Bundle Header -------------
    ///     MajorVersion
    ///     MinorVersion
    ///     NumEmbeddedFiles
    ///     ExtractionID
    ///     DepsJson Location [Version 2+]
    ///        Offset
    ///        Size
    ///     RuntimeConfigJson Location [Version 2+]
    ///        Offset
    ///        Size
    ///     Flags [Version 2+]
    /// - - - - - - Manifest Entries - - - - - - - - - - -
    ///     Series of FileEntries (for each embedded file)
    ///     [File Type, Name, Offset, Size information]
    ///
    ///
    ///
    /// _________________________________________________
    /// </summary>
    public class Manifest
    {
        // NetcoreApp3CompatMode flag is set on a .net5+ app,
        // which chooses to build single-file apps in .netcore3.x compat mode,
        // by constructing the bundler with BundleAllContent option.
        // This mode is expected to be deprecated in future versions of .NET.
        [Flags]
        private enum HeaderFlags : ulong
        {
            None = 0,
            NetcoreApp3CompatMode = 1
        }

        // Bundle ID is a string that is used to uniquely
        // identify this bundle. It is chosen to be compatible
        // with path-names so that the AppHost can use it in
        // extraction path.
        public string BundleID { get; private set; }
        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;
        private FileEntry DepsJsonEntry;
        private FileEntry RuntimeConfigJsonEntry;
        private readonly HeaderFlags Flags;
        public List<FileEntry> Files;
        public string BundleVersion => $"{BundleMajorVersion}.{BundleMinorVersion}";

        public Manifest(uint bundleMajorVersion, bool netcoreapp3CompatMode = false)
        {
            BundleMajorVersion = bundleMajorVersion;
            Files = new List<FileEntry>();
            Flags = (netcoreapp3CompatMode) ? HeaderFlags.NetcoreApp3CompatMode : HeaderFlags.None;
        }

        public FileEntry AddEntry(FileType type, FileStream fileContent, string relativePath, long offset, long compressedSize, uint 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:
                    DepsJsonEntry = entry;
                    break;
                case FileType.RuntimeConfigJson:
                    RuntimeConfigJsonEntry = entry;
                    break;

                case FileType.Assembly:
                    break;

                default:
                    break;
            }

            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;
            string id = Base64Url.EncodeToString(manifestHash).Substring(0, BundleIdLength);
            Debug.Assert(id.Length == BundleIdLength);
            return id;
        }


        public long Write(BinaryWriter writer)
        {
            BundleID ??= GenerateDeterministicId();

            long startOffset = writer.BaseStream.Position;

            // Write the bundle header
            writer.Write(BundleMajorVersion);
            writer.Write(BundleMinorVersion);
            writer.Write(Files.Count);
            writer.Write(BundleID);

            if (BundleMajorVersion >= 2)
            {
                writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Offset : 0);
                writer.Write((DepsJsonEntry != null) ? DepsJsonEntry.Size : 0);

                writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Offset : 0);
                writer.Write((RuntimeConfigJsonEntry != null) ? RuntimeConfigJsonEntry.Size : 0);

                writer.Write((ulong)Flags);
            }

            // Write the manifest entries
            foreach (FileEntry entry in Files)
            {
                entry.Write(writer);
            }
            Debug.Assert(writer.BaseStream.Position - startOffset == GetManifestLength(BundleMajorVersion, Files.Select(static f => f.RelativePath)),
                $"Manifest size mismatch: {writer.BaseStream.Position - startOffset} != {GetManifestLength(BundleMajorVersion, Files.Select(static f => f.RelativePath))}");

            return startOffset;
        }

        /// <summary>
        /// Calculates the length of the manifest in bytes.
        /// </summary>
        public static long GetManifestLength(uint bundleMajorVersion, IEnumerable<string> fileSpecs)
        {
            const string dummyBundleId = "FakeBundleID";
            Debug.Assert(dummyBundleId.Length == BundleIdLength);
            // Size of the header
            long size = sizeof(uint) * 2 + // BundleMajorVersion + BundleMinorVersion
                        sizeof(int) + // NumEmbeddedFiles
                        (bundleMajorVersion >= 2 ? (sizeof(long) * 4 + sizeof(ulong)) : 0); // DepsJson and RuntimeConfigJson offsets and sizes, and Flags
            size += Bundler.GetBinaryWriterStringLength(dummyBundleId);
            // Size of each FileEntry
            foreach (var fileSpec in fileSpecs)
            {
                size += FileEntry.GetFileEntryLength(bundleMajorVersion, fileSpec);
            }

            return size;
        }

        public bool Contains(string relativePath)
        {
            return Files.Any(entry => relativePath.Equals(entry.RelativePath));
        }
    }
}