2020-08-11 12:15:03 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
2020-08-11 15:24:02 +00:00
|
|
|
|
using System.Linq;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
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.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 Reader : IReader
|
2020-08-11 12:15:03 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public const int HeaderLength = 0x24;
|
2020-08-11 13:41:30 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
internal uint _fileCount;
|
|
|
|
|
internal uint _folderCount;
|
|
|
|
|
internal uint _folderRecordOffset;
|
|
|
|
|
private Lazy<FolderRecord[]> _folders = null!;
|
|
|
|
|
private Lazy<Dictionary<string, FolderRecord>> _foldersByName = null!;
|
|
|
|
|
internal string _magic = string.Empty;
|
|
|
|
|
public IStreamFactory _streamFactory = new NativeFileStreamFactory(default);
|
|
|
|
|
internal uint _totalFileNameLength;
|
|
|
|
|
internal uint _totalFolderNameLength;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public VersionType HeaderType { get; private set; }
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public ArchiveFlags ArchiveFlags { get; private set; }
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public FileFlags FileFlags { get; private set; }
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public IEnumerable<FolderRecord> Folders => _folders.Value;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public bool HasFolderNames => ArchiveFlags.HasFlag(ArchiveFlags.HasFolderNames);
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public bool HasFileNames => ArchiveFlags.HasFlag(ArchiveFlags.HasFileNames);
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public bool CompressedByDefault => ArchiveFlags.HasFlag(ArchiveFlags.Compressed);
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public bool Bit9Set => ArchiveFlags.HasFlag(ArchiveFlags.HasFileNameBlobs);
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public bool HasNameBlobs => HeaderType is VersionType.FO3 or VersionType.SSE && Bit9Set;
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public IEnumerable<IFile> Files => _folders.Value.SelectMany(f => f.Files);
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public IArchive State => new BSAState
|
|
|
|
|
{
|
|
|
|
|
Magic = _magic,
|
|
|
|
|
Version = (uint) HeaderType,
|
|
|
|
|
ArchiveFlags = (uint) ArchiveFlags,
|
|
|
|
|
FileFlags = (uint) FileFlags
|
|
|
|
|
};
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public static async ValueTask<Reader> Load(IStreamFactory factory)
|
|
|
|
|
{
|
|
|
|
|
await using var stream = await factory.GetStream().ConfigureAwait(false);
|
|
|
|
|
using var br = new BinaryReader(stream);
|
|
|
|
|
var bsa = new Reader {_streamFactory = factory};
|
|
|
|
|
bsa.LoadHeaders(br);
|
|
|
|
|
return bsa;
|
|
|
|
|
}
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2020-09-04 21:00:29 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public static Reader Load(AbsolutePath filename)
|
|
|
|
|
{
|
|
|
|
|
var bsa = new Reader {_streamFactory = new NativeFileStreamFactory(filename)};
|
|
|
|
|
using var rdr = bsa.GetStream();
|
|
|
|
|
bsa.LoadHeaders(rdr);
|
|
|
|
|
return bsa;
|
|
|
|
|
}
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
internal BinaryReader GetStream()
|
|
|
|
|
{
|
|
|
|
|
return new BinaryReader(_streamFactory.GetStream().Result);
|
|
|
|
|
}
|
2020-08-11 13:41:30 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
private void LoadHeaders(BinaryReader rdr)
|
|
|
|
|
{
|
|
|
|
|
var fourcc = Encoding.ASCII.GetString(rdr.ReadBytes(4));
|
|
|
|
|
|
|
|
|
|
if (fourcc != "BSA\0")
|
|
|
|
|
throw new InvalidDataException("Archive is not a BSA");
|
|
|
|
|
|
|
|
|
|
_magic = fourcc;
|
|
|
|
|
HeaderType = (VersionType) rdr.ReadUInt32();
|
|
|
|
|
_folderRecordOffset = rdr.ReadUInt32();
|
|
|
|
|
ArchiveFlags = (ArchiveFlags) rdr.ReadUInt32();
|
|
|
|
|
_folderCount = rdr.ReadUInt32();
|
|
|
|
|
_fileCount = rdr.ReadUInt32();
|
|
|
|
|
_totalFolderNameLength = rdr.ReadUInt32();
|
|
|
|
|
_totalFileNameLength = rdr.ReadUInt32();
|
|
|
|
|
FileFlags = (FileFlags) rdr.ReadUInt32();
|
|
|
|
|
|
|
|
|
|
_folders = new Lazy<FolderRecord[]>(
|
|
|
|
|
isThreadSafe: true,
|
|
|
|
|
valueFactory: () => LoadFolderRecords());
|
|
|
|
|
_foldersByName = new Lazy<Dictionary<string, FolderRecord>>(
|
|
|
|
|
isThreadSafe: true,
|
|
|
|
|
valueFactory: GetFolderDictionary);
|
|
|
|
|
}
|
2020-08-11 12:15:03 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
private FolderRecord[] LoadFolderRecords()
|
|
|
|
|
{
|
|
|
|
|
using var rdr = GetStream();
|
|
|
|
|
rdr.BaseStream.Position = _folderRecordOffset;
|
|
|
|
|
var folderHeaderLength = FolderRecord.HeaderLength(HeaderType);
|
|
|
|
|
ReadOnlyMemorySlice<byte> folderHeaderData =
|
|
|
|
|
rdr.ReadBytes(checked((int) (folderHeaderLength * _folderCount)));
|
|
|
|
|
|
|
|
|
|
var ret = new FolderRecord[_folderCount];
|
|
|
|
|
for (var idx = 0; idx < _folderCount; idx += 1)
|
|
|
|
|
ret[idx] = new FolderRecord(this, folderHeaderData.Slice(idx * folderHeaderLength, folderHeaderLength),
|
|
|
|
|
idx);
|
|
|
|
|
|
|
|
|
|
// Slice off appropriate file header data per folder
|
|
|
|
|
var fileCountTally = 0;
|
|
|
|
|
foreach (var folder in ret)
|
2020-08-11 12:15:03 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
folder.ProcessFileRecordHeadersBlock(rdr, fileCountTally);
|
|
|
|
|
fileCountTally = checked(fileCountTally + folder.FileCount);
|
2020-08-11 12:15:03 +00:00
|
|
|
|
}
|
2020-08-11 15:29:37 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
if (HasFileNames)
|
2020-08-11 15:29:37 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
var filenameBlock = new FileNameBlock(this, rdr.BaseStream.Position);
|
|
|
|
|
foreach (var folder in ret) folder.FileNameBlock = filenameBlock;
|
2020-08-11 15:29:37 +00:00
|
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Dictionary<string, FolderRecord> GetFolderDictionary()
|
|
|
|
|
{
|
|
|
|
|
if (!HasFolderNames)
|
|
|
|
|
throw new ArgumentException("Cannot get folders by name if the BSA does not have folder names.");
|
|
|
|
|
return _folders.Value.ToDictionary(folder => folder.Name!);
|
2020-08-11 12:15:03 +00:00
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
}
|