JD2022-TU1/main/tools/apps/Packager/Tasks/ReplaceFilesInBundleTask.cs

335 lines
No EOL
13 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using JD.Collections.Extensions;
using JD.Core.ProjectManaging;
using JD.Serializers.Stream;
using NUnit.Framework;
using Packager.Parameters;
using PluginCommon.Bundle;
using PluginCommon.Bundle.Core;
using TeaBox;
using DictionaryExtensions = Ubisoft.Collections.DictionaryExtensions;
using File = System.IO.File;
using LogCategory = JD.LogService.LogCategory;
namespace Packager.Tasks
{
internal class BundleHeaders
{
public BundleBootHeader BootHeader { get; set; }
public List<FileHeaderBuild> FileHeaders { get; set; }
public BundleHeaders()
{
BootHeader = new BundleBootHeader();
FileHeaders = new List<FileHeaderBuild>();
}
}
internal class BundleFileMap : Dictionary<string, List<string>>
{
public void Add(string bundlePath, string filePath)
{
if (ContainsKey(bundlePath))
{
this[bundlePath].Add(filePath);
}
else
{
Add(bundlePath, new List<string> { filePath });
}
}
}
/// <summary>
/// In the bundle binary stream we just have to :
/// 1. find the file headers of the files that has to be replaced.
/// 2. construct the their file headers again.
/// 3. append their modified content stream at the end of the binary stream.
///
/// Information needed:
/// 1. composition of the binary .ipk bundle file?
/// The bundle is divided into 3 parts:
/// 1. bootHeader
/// 2. List of file headers
/// 3. data/content of each file
///
/// 2. how is the bundle binary file content read?
/// The content is read using file headers. The file header has the starting location of their respective content.
/// There is a header list in which would be traversed one by one, and thus all the binary content is read.
///
/// 3. what is the role of the "fileStart" location, in bundle's boot header?
/// This information is needed while writing the files' content in the bundle to mark the starting location where the content writing starts.
///
/// The file header's the field: 'Position' denotes the location of the content respective to that file header.
/// eg. If 6024, is the Position in the file header. Then this is the location of this file's content relative to the starting location of data section of the bundle,
/// the real absolute location for this content in the entire binary stream is :
/// "fileStart Location" + 6024.
/// |
/// |------> = sizeof(bootHeader + all fileHeaders)
///
/// Since, the size of the file header is fixed, and we already know how many file headers are actually there,
/// and the size of the boot header is also fixed, this way we know the exact location from where start reading the file header to put them in a list.
/// </summary>
public class ReplaceFilesInBundleTask : AssetCookerTask
{
[Input("Bundles to Inject In", IOCardinality.Multi, IOType.File)]
public ReferenceList Bundles { get; set; }
[Input("Files to inject in the bundles", IOCardinality.Multi, IOType.String)]
public ReferenceList FilestoInject { get; set; }
private readonly Dictionary<string, ulong> _bundlePointerLocations = new Dictionary<string, ulong>();
private readonly BundleFileMap _bundleFileMap = new BundleFileMap();
public ReplaceFilesInBundleTask()
{
Bundles = new ReferenceList();
FilestoInject = new ReferenceList();
CookList = true;
}
protected override void StartInternal(BuildConfiguration profile, cookerWrapper cooker)
{
foreach (var bundlePath in Bundles)
{
ModifyBundle(bundlePath);
}
LogTaskInformation();
}
internal void LogTaskInformation()
{
var taskInfo = new StringBuilder();
taskInfo.Append("Files replaced : \n");
FilestoInject.ForEach(x => taskInfo.Append(x + "\n"));
taskInfo.Append("Bundles modified: \n");
Bundles.ForEach(bundle => taskInfo.Append(bundle + "\n"));
LogMessage(taskInfo.ToString(), LogCategory.Message);
}
public void ModifyBundle(string bundlePath)
{
var bundleHeader = GetBundleHeaderInfo(bundlePath);
if (ModifyBundleFileHeaders(bundlePath, bundleHeader))
{
using (var bundleStream = File.Open(bundlePath, FileMode.Open, FileAccess.ReadWrite))
{
UpdateOriginalBundleHeaders(bundleStream, bundleHeader);
UpdateOriginalBundleContent(bundlePath, bundleStream);
}
}
}
internal BundleHeaders GetBundleHeaderInfo(string bundlePath)
{
var bundleHeaderInfo = new BundleHeaders();
using (var bundle = new Bundle(bundlePath))
{
bundleHeaderInfo.BootHeader = bundle.BootHeader;
bundleHeaderInfo.FileHeaders.AddRange(bundle.FileHeaders);
// maintaining a dictionary to keep the track of, end of stream for the bundle's binary stream.
DictionaryExtensions.AddOrUpdate(_bundlePointerLocations, bundlePath, (ulong) bundle.GetBundleBinaryStreamLength(), UpdateValueFactory);
}
return bundleHeaderInfo;
}
private ulong UpdateValueFactory(string key, ulong value)
{
_bundlePointerLocations[key] = value;
return value;
}
private bool ModifyBundleFileHeaders(string bundlePath, BundleHeaders bundleHeader)
{
var isHeaderModified = false;
FilestoInject.ForEach(file =>
{
if (ModifyFileHeader(file, bundleHeader, bundlePath))
{
UpdateBundlePointer(file, bundlePath);
_bundleFileMap.Add(bundlePath, file);
isHeaderModified = true;
}
});
return isHeaderModified;
}
private static void UpdateOriginalBundleHeaders(FileStream bundleStream, BundleHeaders bundleHeader)
{
var bundleWriter = new BundleWriter(bundleStream, bundleHeader.BootHeader, bundleHeader.FileHeaders);
bundleWriter.WriteHeader();
}
private void UpdateOriginalBundleContent(string bundlePath, FileStream bundleStream)
{
_bundleFileMap[bundlePath].ForEach(file =>
{
bundleStream.Seek(0, SeekOrigin.End);
var fileStream = File.Open(DataPath.GetAbsolutePath(file), FileMode.Open, FileAccess.Read);
fileStream.AppendTo(bundleStream);
fileStream.Close();
});
}
internal bool ModifyFileHeader(string filePath, BundleHeaders bundleHeader, string bundlePath)
{
var fileHeader = bundleHeader.FileHeaders.FirstOrDefault(file => file.FilePath.Contains(filePath));
if (fileHeader == null)
return false;
fileHeader.RuntimeHeader.Positions[0] = _bundlePointerLocations[bundlePath] - bundleHeader.BootHeader.FilesStart;
UpdateSizeInformationInHeader(filePath, fileHeader);
return true;
}
private void UpdateSizeInformationInHeader(string filePath, FileHeaderBuild fileHeader)
{
var fileDataStream = File.Open(DataPath.GetAbsolutePath(filePath), FileMode.Open, FileAccess.Read);
fileHeader.RuntimeHeader.OriginalSize = (uint)fileDataStream.Length;
fileHeader.RuntimeHeader.CompressedSize = GetStreamCompressedSize(fileDataStream);
fileDataStream.Close();
}
private void UpdateBundlePointer(string filePath, string bundlePath)
{
using (var fileContentStream = File.Open(DataPath.GetAbsolutePath(filePath), FileMode.Open, FileAccess.Read))
{
_bundlePointerLocations[bundlePath] += (ulong)fileContentStream.Length;
}
}
/// <summary>
/// this is the function responsible for calculating the compresses length of a stream,
/// the functionality of this function has to be exactly same the compression function in the engine
/// in file X:\JD_CODE\main\src\engine\file\BundleBuilder.cpp, in function : compressFile()
/// </summary>
/// <param name="newContent"></param>
/// <returns></returns>
private uint GetStreamCompressedSize(FileStream newContent)
{
uint compressedSize = 0;
return compressedSize;
}
}
[TestFixture]
internal class TestInjectFilesTask
{
private bool _filesLogged;
private bool _bundlesLogged;
private readonly string _bundlePath =Path.Combine(ProjectManager.Instance.CurrentProject.DataPath, @"output\durango_loose\bundle_durango.ipk");
private readonly string _fileToReplace = @"cache/itf_cooked/durango/enginedata/gameconfig/padrumbleconfig.isg.ckd";
private void Initialization(ReplaceFilesInBundleTask injector)
{
injector.Bundles.Add(_bundlePath);
injector.FilestoInject.Add(_fileToReplace);
injector.InstanceName = "Testing Replacement";
}
private void Logger(object sender, string message)
{
Console.WriteLine(message);
if (message.Contains("Files"))
{
_filesLogged = true;
}
if (message.Contains("Bundles"))
{
_bundlesLogged = true;
}
}
[Test]
public void TestLogFilesToReplace()
{
var injector = new ReplaceFilesInBundleTask();
Initialization(injector);
injector.Logger.OnInfoLogged += Logger;
injector.Logger.OnWarningLogged += Logger;
injector.Logger.OnErrorLogged += Logger;
injector.LogTaskInformation();
Assert.IsTrue(_filesLogged);
Assert.IsTrue(_bundlesLogged);
injector.Logger.OnErrorLogged -= Logger;
injector.Logger.OnWarningLogged -= Logger;
injector.Logger.OnInfoLogged -= Logger;
}
[Test]
public void TestExtractHeaderInfoForBundle()
{
var injector = new ReplaceFilesInBundleTask();
Initialization(injector);
var bundleHeader = injector.GetBundleHeaderInfo(_bundlePath);
Assert.NotNull(bundleHeader);
}
[Test]
public void TestModifyFileHeaderForFileToReplace()
{
var injector = new ReplaceFilesInBundleTask();
Initialization(injector);
var bundleHeader = injector.GetBundleHeaderInfo(_bundlePath);
var runtimeHeaderForFile = bundleHeader.FileHeaders.FirstOrDefault(file => file.FilePath.Contains(_fileToReplace));
if (runtimeHeaderForFile != null)
{
var filePosition = runtimeHeaderForFile.RuntimeHeader.Positions[0];
injector.ModifyFileHeader(_fileToReplace, bundleHeader, _bundlePath);
var postModification = runtimeHeaderForFile.RuntimeHeader.Positions[0];
Assert.AreNotEqual(postModification, filePosition);
return;
}
Assert.IsTrue(true, "the file is not present in the bundle, therefore the test case is invalid");
}
[Test]
public void TestModifiedBundleForReplacedContent()
{
var injector = new ReplaceFilesInBundleTask();
Initialization(injector);
var oldBundleSize = GetBundleStreamSize();
injector.ModifyBundle(_bundlePath);
var newBundleSize = GetBundleStreamSize();
long fileSize;
using (var file = File.Open(DataPath.GetAbsolutePath(_fileToReplace), FileMode.Open, FileAccess.Read))
{
fileSize = file.Length;
}
Assert.AreEqual(oldBundleSize + fileSize, newBundleSize);
}
private long GetBundleStreamSize()
{
var bundle = new Bundle(_bundlePath);
var size = bundle.GetBundleBinaryStreamLength();
bundle.Dispose();
return size;
}
}
}