wabbajack/Wabbajack.Compression.BSA/TES5Archive/Builder.cs

353 lines
12 KiB
C#
Raw Normal View History

2021-09-27 12:42:46 +00:00
using System;
2019-07-28 23:04:23 +00:00
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
2021-09-27 12:42:46 +00:00
using System.Threading;
using System.Threading.Tasks;
2019-09-14 04:35:42 +00:00
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using K4os.Compression.LZ4;
using K4os.Compression.LZ4.Streams;
using Wabbajack.Common;
2021-09-27 12:42:46 +00:00
using Wabbajack.Compression.BSA.Interfaces;
using Wabbajack.Compression.BSA.TES3Archive;
using Wabbajack.DTOs.BSA.ArchiveStates;
using Wabbajack.DTOs.BSA.FileStates;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack.Compression.BSA.TES5Archive
2019-07-28 23:04:23 +00:00
{
2021-09-27 12:42:46 +00:00
public class Builder : IBuilder
2019-07-28 23:04:23 +00:00
{
2019-09-14 04:35:42 +00:00
internal byte[] _fileId;
2019-07-28 23:04:23 +00:00
2021-09-27 12:42:46 +00:00
private List<FileEntry> _files = new();
internal List<FolderRecordBuilder> _folders = new();
2019-09-14 04:35:42 +00:00
internal uint _offset;
internal DiskSlabAllocator _slab;
2021-09-27 12:42:46 +00:00
internal uint _totalFileNameLength;
2019-09-14 04:35:42 +00:00
public IEnumerable<FileEntry> Files => _files;
2020-08-11 12:14:12 +00:00
public ArchiveFlags ArchiveFlags { get; set; }
2019-07-28 23:04:23 +00:00
2020-08-11 12:14:12 +00:00
public FileFlags FileFlags { get; set; }
2019-07-28 23:04:23 +00:00
2020-08-11 12:14:12 +00:00
public VersionType HeaderType { get; set; }
2019-07-28 23:04:23 +00:00
2020-03-23 12:57:18 +00:00
public IEnumerable<RelativePath> FolderNames
2019-07-28 23:04:23 +00:00
{
2021-09-27 12:42:46 +00:00
get { return _files.Select(f => f.Path.Parent).Distinct(); }
2019-07-28 23:04:23 +00:00
}
2020-08-11 12:14:12 +00:00
public bool HasFolderNames => ArchiveFlags.HasFlag(ArchiveFlags.HasFileNames);
2019-07-28 23:04:23 +00:00
2020-08-11 12:14:12 +00:00
public bool HasFileNames => ArchiveFlags.HasFlag(ArchiveFlags.HasFileNames);
2019-09-14 04:35:42 +00:00
2020-08-11 12:14:12 +00:00
public bool CompressedByDefault => ArchiveFlags.HasFlag(ArchiveFlags.Compressed);
2019-09-14 04:35:42 +00:00
2020-08-11 12:14:12 +00:00
public bool HasNameBlobs => ArchiveFlags.HasFlag(ArchiveFlags.HasFileNameBlobs);
2019-07-28 23:04:23 +00:00
2021-09-27 12:42:46 +00:00
public async ValueTask AddFile(AFile state, Stream src, CancellationToken token)
2019-07-28 23:04:23 +00:00
{
2021-09-27 12:42:46 +00:00
var bsaState = (BSAFile)state;
2021-09-27 12:42:46 +00:00
var r = await FileEntry.Create(this, bsaState.Path, src, bsaState.FlipCompression, token);
lock (this)
{
_files.Add(r);
}
}
2021-09-27 12:42:46 +00:00
public async ValueTask Build(Stream fs, CancellationToken token)
2019-07-28 23:04:23 +00:00
{
RegenFolderRecords();
2021-09-27 12:42:46 +00:00
await using var wtr = new BinaryWriter(fs, Encoding.Default, true);
wtr.Write(_fileId);
2020-08-11 12:14:12 +00:00
wtr.Write((uint)HeaderType);
wtr.Write(_offset);
2020-08-11 12:14:12 +00:00
wtr.Write((uint)ArchiveFlags);
var folders = FolderNames.ToList();
2021-09-27 12:42:46 +00:00
wtr.Write((uint)folders.Count);
wtr.Write((uint)_files.Count);
wtr.Write((uint)_folders.Select(f => f._nameBytes.Length - 1).Sum()); // totalFolderNameLength
var s = _files.Select(f => f._pathBytes.Length).Sum();
_totalFileNameLength = (uint)_files.Select(f => f._nameBytes.Length).Sum();
wtr.Write(_totalFileNameLength); // totalFileNameLength
2020-08-11 12:14:12 +00:00
wtr.Write((uint)FileFlags);
foreach (var folder in _folders) folder.WriteFolderRecord(wtr);
foreach (var folder in _folders)
2019-07-28 23:04:23 +00:00
{
if (HasFolderNames)
wtr.Write(folder._nameBytes);
foreach (var file in folder._files) file.WriteFileRecord(wtr);
}
2021-09-27 12:42:46 +00:00
foreach (var file in _files)
await wtr.BaseStream.WriteAsync(file._nameBytes, token);
foreach (var file in _files)
await file.WriteData(wtr, token);
}
public static Builder Create(TemporaryFileManager tempGenerator)
{
var self = new Builder
{
_fileId = Encoding.ASCII.GetBytes("BSA\0"),
_offset = 0x24,
_slab = new DiskSlabAllocator(tempGenerator)
};
return self;
}
public static Builder Create(BSAState bsaStateObject, TemporaryFileManager tempGenerator)
{
var self = Create(tempGenerator);
self.HeaderType = (VersionType)bsaStateObject.Version;
self.FileFlags = (FileFlags)bsaStateObject.FileFlags;
self.ArchiveFlags = (ArchiveFlags)bsaStateObject.ArchiveFlags;
return self;
}
2021-09-27 12:42:46 +00:00
public async ValueTask DisposeAsync()
{
await _slab.DisposeAsync();
2019-07-28 23:04:23 +00:00
}
public void RegenFolderRecords()
{
2020-03-23 12:57:18 +00:00
_folders = _files.GroupBy(f => f.Path.Parent)
2019-09-14 04:35:42 +00:00
.Select(f => new FolderRecordBuilder(this, f.Key, f.ToList()))
.OrderBy(f => f._hash)
.ToList();
foreach (var folder in _folders)
2019-09-14 04:35:42 +00:00
foreach (var file in folder._files)
file._folder = folder;
_files = (from folder in _folders
2019-09-14 04:35:42 +00:00
from file in folder._files
orderby folder._hash, file._hash
select file).ToList();
2019-07-28 23:04:23 +00:00
}
}
public class FolderRecordBuilder
{
2021-09-27 12:42:46 +00:00
internal Builder _bsa;
2019-07-28 23:04:23 +00:00
internal uint _fileCount;
2019-09-14 04:35:42 +00:00
internal IEnumerable<FileEntry> _files;
internal ulong _hash;
2019-07-28 23:04:23 +00:00
internal byte[] _nameBytes;
internal ulong _offset;
2019-09-14 04:35:42 +00:00
internal uint _recordSize;
2019-07-28 23:04:23 +00:00
2021-09-27 12:42:46 +00:00
public FolderRecordBuilder(Builder bsa, RelativePath folderName, IEnumerable<FileEntry> files)
{
2019-09-14 04:35:42 +00:00
_files = files.OrderBy(f => f._hash);
2020-03-23 12:57:18 +00:00
Name = folderName;
2019-09-14 04:35:42 +00:00
_bsa = bsa;
// Folders don't have extensions, so let's make sure we cut it out
_hash = Name.GetFolderBSAHash();
2021-09-27 12:42:46 +00:00
_fileCount = (uint)files.Count();
2019-09-14 04:35:42 +00:00
_nameBytes = folderName.ToBZString(_bsa.HeaderType);
_recordSize = sizeof(ulong) + sizeof(uint) + sizeof(uint);
}
2019-09-14 04:35:42 +00:00
public ulong Hash => _hash;
2020-03-23 12:57:18 +00:00
public RelativePath Name { get; }
2019-07-28 23:04:23 +00:00
public ulong SelfSize
{
get
{
if (_bsa.HeaderType == VersionType.SSE)
return sizeof(ulong) + sizeof(uint) + sizeof(uint) + sizeof(ulong);
2019-09-14 04:35:42 +00:00
return sizeof(ulong) + sizeof(uint) + sizeof(uint);
2019-07-28 23:04:23 +00:00
}
}
public ulong FileRecordSize
{
get
{
ulong size = 0;
if (_bsa.HasFolderNames)
2021-09-27 12:42:46 +00:00
size += (ulong)_nameBytes.Length;
size += (ulong)_files.Select(f => sizeof(ulong) + sizeof(uint) + sizeof(uint)).Sum();
2019-07-28 23:04:23 +00:00
return size;
}
}
public void WriteFolderRecord(BinaryWriter wtr)
2019-07-28 23:04:23 +00:00
{
var idx = _bsa._folders.IndexOf(this);
2021-09-27 12:42:46 +00:00
_offset = (ulong)wtr.BaseStream.Position;
_offset += (ulong)_bsa._folders.Skip(idx).Select(f => (long)f.SelfSize).Sum();
2019-07-28 23:04:23 +00:00
_offset += _bsa._totalFileNameLength;
2021-09-27 12:42:46 +00:00
_offset += (ulong)_bsa._folders.Take(idx).Select(f => (long)f.FileRecordSize).Sum();
2019-07-28 23:04:23 +00:00
2019-09-14 04:35:42 +00:00
var sp = wtr.BaseStream.Position;
2019-07-28 23:04:23 +00:00
wtr.Write(_hash);
wtr.Write(_fileCount);
if (_bsa.HeaderType == VersionType.SSE)
{
2021-09-27 12:42:46 +00:00
wtr.Write((uint)0); // unk
2019-09-14 04:35:42 +00:00
wtr.Write(_offset); // offset
2019-07-28 23:04:23 +00:00
}
2021-09-27 12:42:46 +00:00
else if (_bsa.HeaderType is VersionType.FO3 or VersionType.TES4)
{
2021-09-27 12:42:46 +00:00
wtr.Write((uint)_offset);
}
else
{
throw new NotImplementedException($"Cannot write to BSAs of type {_bsa.HeaderType}");
}
2019-07-28 23:04:23 +00:00
}
}
public class FileEntry
{
2021-09-27 12:42:46 +00:00
internal Builder _bsa;
2019-07-28 23:04:23 +00:00
internal bool _flipCompression;
2019-09-14 04:35:42 +00:00
internal FolderRecordBuilder _folder;
2019-07-28 23:04:23 +00:00
internal ulong _hash;
2019-09-14 04:35:42 +00:00
internal string _name;
2019-07-28 23:04:23 +00:00
internal byte[] _nameBytes;
2019-09-14 04:35:42 +00:00
private long _offsetOffset;
internal int _originalSize;
2020-03-23 12:57:18 +00:00
internal RelativePath _path;
private byte[] _pathBSBytes;
2019-07-28 23:04:23 +00:00
internal byte[] _pathBytes;
private Stream _srcData;
2019-07-28 23:04:23 +00:00
2019-09-14 04:35:42 +00:00
public bool Compressed
{
get
{
if (_flipCompression)
return !_bsa.CompressedByDefault;
return _bsa.CompressedByDefault;
}
2019-07-28 23:04:23 +00:00
}
2020-03-23 12:57:18 +00:00
public RelativePath Path => _path;
2019-09-14 04:35:42 +00:00
public bool FlipCompression => _flipCompression;
public ulong Hash => _hash;
public FolderRecordBuilder Folder => _folder;
2021-09-27 12:42:46 +00:00
public static async Task<FileEntry> Create(Builder bsa, RelativePath path, Stream src, bool flipCompression,
CancellationToken token)
{
var entry = new FileEntry();
entry._bsa = bsa;
entry._path = path;
entry._name = (string)entry._path.FileName;
entry._hash = entry._name.GetBSAHash();
entry._nameBytes = entry._name.ToTermString(bsa.HeaderType);
entry._pathBytes = entry._path.ToTermString(bsa.HeaderType);
entry._pathBSBytes = entry._path.ToBSString();
entry._flipCompression = flipCompression;
entry._srcData = src;
entry._originalSize = (int)entry._srcData.Length;
if (entry.Compressed)
await entry.CompressData(token);
return entry;
}
private async Task CompressData(CancellationToken token)
2019-07-28 23:04:23 +00:00
{
switch (_bsa.HeaderType)
2019-07-28 23:04:23 +00:00
{
case VersionType.SSE:
2019-09-14 04:35:42 +00:00
{
var r = new MemoryStream();
2021-09-27 12:42:46 +00:00
await using (var w = LZ4Stream.Encode(r,
new LZ4EncoderSettings { CompressionLevel = LZ4Level.L12_MAX }, true))
{
2021-09-27 12:42:46 +00:00
await _srcData.CopyToWithStatusAsync(_srcData.Length, w, token);
}
await _srcData.DisposeAsync();
_srcData = _bsa._slab.Allocate(r.Length);
r.Position = 0;
2021-09-27 12:42:46 +00:00
await r.CopyToWithStatusAsync(r.Length, _srcData, token);
_srcData.Position = 0;
break;
2019-09-14 04:35:42 +00:00
}
case VersionType.FO3:
case VersionType.TES4:
2019-09-14 04:35:42 +00:00
{
var r = new MemoryStream();
using (var w = new DeflaterOutputStream(r))
{
w.IsStreamOwner = false;
2021-09-27 12:42:46 +00:00
await _srcData.CopyToWithStatusAsync(_srcData.Length, w, token);
}
await _srcData.DisposeAsync();
_srcData = _bsa._slab.Allocate(r.Length);
r.Position = 0;
2021-09-27 12:42:46 +00:00
await r.CopyToWithStatusAsync(r.Length, _srcData, token);
_srcData.Position = 0;
break;
2019-09-14 04:35:42 +00:00
}
default:
throw new NotImplementedException($"Can't compress data for {_bsa.HeaderType} BSAs.");
2019-07-28 23:04:23 +00:00
}
}
internal void WriteFileRecord(BinaryWriter wtr)
2019-07-28 23:04:23 +00:00
{
wtr.Write(_hash);
var size = _srcData.Length;
2019-09-14 04:35:42 +00:00
if (_bsa.HasNameBlobs) size += _pathBSBytes.Length;
if (Compressed) size += 4;
if (_flipCompression)
2021-09-27 12:42:46 +00:00
wtr.Write((uint)size | (0x1 << 30));
else
2021-09-27 12:42:46 +00:00
wtr.Write((uint)size);
_offsetOffset = wtr.BaseStream.Position;
2019-09-14 04:35:42 +00:00
wtr.Write(0xDEADBEEF);
}
2021-09-27 12:42:46 +00:00
internal async Task WriteData(BinaryWriter wtr, CancellationToken token)
{
2021-09-27 12:42:46 +00:00
var offset = (uint)wtr.BaseStream.Position;
wtr.BaseStream.Position = _offsetOffset;
2019-09-14 04:35:42 +00:00
wtr.Write(offset);
wtr.BaseStream.Position = offset;
2019-09-14 04:35:42 +00:00
if (_bsa.HasNameBlobs) wtr.Write(_pathBSBytes);
if (Compressed)
{
2021-09-27 12:42:46 +00:00
wtr.Write((uint)_originalSize);
_srcData.Position = 0;
2021-09-27 12:42:46 +00:00
await _srcData.CopyToLimitAsync(wtr.BaseStream, (int)_srcData.Length, token);
await _srcData.DisposeAsync();
}
else
{
_srcData.Position = 0;
2021-09-27 12:42:46 +00:00
await _srcData.CopyToLimitAsync(wtr.BaseStream, (int)_srcData.Length, token);
await _srcData.DisposeAsync();
}
2019-07-28 23:04:23 +00:00
}
}
2021-09-27 12:42:46 +00:00
}