2020-08-11 12:15:03 +00:00
|
|
|
|
using System;
|
2020-08-11 13:41:30 +00:00
|
|
|
|
using System.Buffers.Binary;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
using System.IO;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
using System.Threading;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
|
|
|
|
|
using K4os.Compression.LZ4.Streams;
|
|
|
|
|
using Wabbajack.Common;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
using Wabbajack.Compression.BSA.Interfaces;
|
|
|
|
|
using Wabbajack.DTOs.BSA.FileStates;
|
|
|
|
|
using Wabbajack.DTOs.Streams;
|
|
|
|
|
using Wabbajack.Paths;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
namespace Wabbajack.Compression.BSA.TES5Archive;
|
|
|
|
|
|
|
|
|
|
public class FileRecord : IFile
|
2020-08-11 12:15:03 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public const int HeaderLength = 0x10;
|
|
|
|
|
|
|
|
|
|
private readonly ReadOnlyMemorySlice<byte> _headerData;
|
|
|
|
|
internal readonly int _index;
|
|
|
|
|
internal readonly Lazy<string> _name;
|
|
|
|
|
internal readonly FileNameBlock _nameBlock;
|
|
|
|
|
internal readonly int _overallIndex;
|
|
|
|
|
internal Lazy<(uint Size, uint OnDisk, uint Original)> _size;
|
|
|
|
|
|
|
|
|
|
internal FileRecord(
|
|
|
|
|
FolderRecord folderRecord,
|
|
|
|
|
ReadOnlyMemorySlice<byte> data,
|
|
|
|
|
int index,
|
|
|
|
|
int overallIndex,
|
|
|
|
|
FileNameBlock nameBlock)
|
2020-08-11 12:15:03 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
_index = index;
|
|
|
|
|
_overallIndex = overallIndex;
|
|
|
|
|
_headerData = data;
|
|
|
|
|
_nameBlock = nameBlock;
|
|
|
|
|
Folder = folderRecord;
|
|
|
|
|
_name = new Lazy<string>(GetName, LazyThreadSafetyMode.PublicationOnly);
|
|
|
|
|
|
|
|
|
|
// Will be replaced if CopyDataTo is called before value is created
|
|
|
|
|
_size = new Lazy<(uint Size, uint OnDisk, uint Original)>(
|
|
|
|
|
mode: LazyThreadSafetyMode.ExecutionAndPublication,
|
|
|
|
|
valueFactory: () =>
|
|
|
|
|
{
|
|
|
|
|
using var rdr = BSA.GetStream();
|
|
|
|
|
rdr.BaseStream.Position = Offset;
|
|
|
|
|
return ReadSize(rdr);
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public ulong Hash => BinaryPrimitives.ReadUInt64LittleEndian(_headerData);
|
|
|
|
|
protected uint RawSize => BinaryPrimitives.ReadUInt32LittleEndian(_headerData.Slice(0x8));
|
|
|
|
|
public uint Offset => BinaryPrimitives.ReadUInt32LittleEndian(_headerData.Slice(0xC));
|
|
|
|
|
public string Name => _name.Value;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public bool FlipCompression => (RawSize & (0x1 << 30)) > 0;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
internal FolderRecord Folder { get; }
|
|
|
|
|
internal Reader BSA => Folder.BSA;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public bool Compressed
|
|
|
|
|
{
|
|
|
|
|
get
|
2020-08-11 12:15:03 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
if (FlipCompression) return !BSA.CompressedByDefault;
|
|
|
|
|
return BSA.CompressedByDefault;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
|
}
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public uint Size => _size.Value.Size;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public RelativePath Path =>
|
|
|
|
|
(string.IsNullOrEmpty(Folder.Name) ? Name : Folder.Name + "\\" + Name).ToRelativePath();
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public AFile State => new BSAFile
|
|
|
|
|
{
|
|
|
|
|
FlipCompression = FlipCompression,
|
|
|
|
|
Index = _index,
|
|
|
|
|
Path = Path
|
|
|
|
|
};
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public async ValueTask CopyDataTo(Stream output, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
await using var in_file = await BSA._streamFactory.GetStream().ConfigureAwait(false);
|
|
|
|
|
using var rdr = new BinaryReader(in_file);
|
|
|
|
|
rdr.BaseStream.Position = Offset;
|
2020-08-11 13:41:30 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
var size = ReadSize(rdr);
|
|
|
|
|
if (!_size.IsValueCreated) _size = new Lazy<(uint Size, uint OnDisk, uint Original)>(size);
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
if (BSA.HeaderType == VersionType.SSE)
|
|
|
|
|
{
|
|
|
|
|
if (Compressed && size.Size != size.OnDisk)
|
2020-08-11 12:15:03 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
await using var r = LZ4Stream.Decode(rdr.BaseStream);
|
|
|
|
|
await r.CopyToLimitAsync(output, (int) size.Original, token).ConfigureAwait(false);
|
2020-08-11 12:15:03 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
await rdr.BaseStream.CopyToLimitAsync(output, (int) size.OnDisk, token).ConfigureAwait(false);
|
2020-08-11 13:41:30 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
|
else
|
2021-09-27 12:42:46 +00:00
|
|
|
|
{
|
2020-08-11 13:41:30 +00:00
|
|
|
|
if (Compressed)
|
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
await using var z = new InflaterInputStream(rdr.BaseStream);
|
|
|
|
|
await z.CopyToLimitAsync(output, (int) size.Original, token).ConfigureAwait(false);
|
2020-08-11 13:41:30 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
await rdr.BaseStream.CopyToLimitAsync(output, (int) size.OnDisk, token).ConfigureAwait(false);
|
2020-08-11 13:41:30 +00:00
|
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-08-11 13:41:30 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public async ValueTask<IStreamFactory> GetStreamFactory(CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
var ms = new MemoryStream();
|
|
|
|
|
await CopyDataTo(ms, token);
|
|
|
|
|
ms.Position = 0;
|
|
|
|
|
return new MemoryStreamFactory(ms, Path, BSA._streamFactory.LastModifiedUtc);
|
|
|
|
|
}
|
2020-08-11 13:41:30 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
private string GetName()
|
|
|
|
|
{
|
|
|
|
|
var names = _nameBlock.Names.Value;
|
|
|
|
|
return names[_overallIndex].ReadStringTerm(BSA.HeaderType);
|
|
|
|
|
}
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
private (uint Size, uint OnDisk, uint Original) ReadSize(BinaryReader rdr)
|
|
|
|
|
{
|
|
|
|
|
var size = RawSize;
|
|
|
|
|
if (FlipCompression)
|
|
|
|
|
size = size ^ (0x1 << 30);
|
|
|
|
|
|
|
|
|
|
if (Compressed)
|
|
|
|
|
size -= 4;
|
|
|
|
|
|
|
|
|
|
byte nameBlobOffset;
|
|
|
|
|
if (BSA.HasNameBlobs)
|
|
|
|
|
{
|
|
|
|
|
nameBlobOffset = rdr.ReadByte();
|
|
|
|
|
// Just skip, not using
|
|
|
|
|
rdr.BaseStream.Position += nameBlobOffset;
|
|
|
|
|
// Minus one more for the size of the name blob offset size
|
|
|
|
|
nameBlobOffset++;
|
|
|
|
|
}
|
|
|
|
|
else
|
2020-08-11 12:15:03 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
nameBlobOffset = 0;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
|
|
|
|
|
|
uint originalSize;
|
|
|
|
|
if (Compressed)
|
|
|
|
|
originalSize = rdr.ReadUInt32();
|
|
|
|
|
else
|
|
|
|
|
originalSize = 0;
|
|
|
|
|
|
|
|
|
|
var onDiskSize = size - nameBlobOffset;
|
|
|
|
|
if (Compressed)
|
|
|
|
|
return (Size: originalSize, OnDisk: onDiskSize, Original: originalSize);
|
|
|
|
|
return (Size: onDiskSize, OnDisk: onDiskSize, Original: originalSize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dump(Action<string> print)
|
|
|
|
|
{
|
|
|
|
|
print($"Name: {Name}");
|
|
|
|
|
print($"Offset: {Offset}");
|
|
|
|
|
print($"Raw Size: {RawSize}");
|
|
|
|
|
print($"Index: {_index}");
|
2020-08-11 12:15:03 +00:00
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
}
|