wabbajack/Compression.BSA/BSAReader.cs

358 lines
9.9 KiB
C#
Raw Normal View History

2019-09-14 04:35:42 +00:00
using System;
2019-07-28 23:04:23 +00:00
using System.Collections.Generic;
using System.IO;
using System.Text;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using K4os.Compression.LZ4.Streams;
2020-03-23 12:57:18 +00:00
using Wabbajack.Common;
2019-08-28 03:22:57 +00:00
using File = Alphaleonis.Win32.Filesystem.File;
2019-07-28 23:04:23 +00:00
namespace Compression.BSA
{
public enum VersionType : uint
{
TES4 = 0x67,
FO3 = 0x68, // FO3, FNV, TES5
2019-07-28 23:04:23 +00:00
SSE = 0x69,
FO4 = 0x01,
TES3 = 0xFF // Not a real Bethesda version number
2019-09-14 04:35:42 +00:00
}
2019-07-28 23:04:23 +00:00
[Flags]
public enum ArchiveFlags : uint
{
HasFolderNames = 0x1,
HasFileNames = 0x2,
Compressed = 0x4,
Unk4 = 0x8,
Unk5 = 0x10,
Unk6 = 0x20,
XBox360Archive = 0x40,
Unk8 = 0x80,
HasFileNameBlobs = 0x100,
Unk10 = 0x200,
Unk11 = 0x400
}
[Flags]
public enum FileFlags : uint
{
Meshes = 0x1,
Textures = 0x2,
Menus = 0x4,
Sounds = 0x8,
Voices = 0x10,
Shaders = 0x20,
Trees = 0x40,
Fonts = 0x80,
Miscellaneous = 0x100
}
public class BSAReader : IDisposable, IBSAReader
2019-07-28 23:04:23 +00:00
{
internal uint _archiveFlags;
internal uint _fileCount;
internal uint _fileFlags;
2020-03-23 12:57:18 +00:00
internal AbsolutePath _fileName;
internal uint _folderCount;
internal uint _folderRecordOffset;
2019-09-14 04:35:42 +00:00
private List<FolderRecord> _folders;
internal string _magic;
2019-09-14 04:35:42 +00:00
private readonly BinaryReader _rdr;
private readonly Stream _stream;
internal uint _totalFileNameLength;
internal uint _totalFolderNameLength;
internal uint _version;
2019-07-28 23:04:23 +00:00
2020-03-23 12:57:18 +00:00
public BSAReader(AbsolutePath filename) : this(filename.OpenRead())
2019-07-28 23:04:23 +00:00
{
2019-09-14 04:35:42 +00:00
_fileName = filename;
2019-07-28 23:04:23 +00:00
}
2019-09-14 04:35:42 +00:00
public BSAReader(Stream stream)
2019-07-28 23:04:23 +00:00
{
2019-09-14 04:35:42 +00:00
_stream = stream;
_rdr = new BinaryReader(_stream);
LoadHeaders();
2019-07-28 23:04:23 +00:00
}
public IEnumerable<IFile> Files
2019-07-28 23:04:23 +00:00
{
get
{
2019-09-14 04:35:42 +00:00
foreach (var folder in _folders)
foreach (var file in folder._files)
yield return file;
2019-07-28 23:04:23 +00:00
}
}
public ArchiveStateObject State => new BSAStateObject(this);
2019-09-14 04:35:42 +00:00
public VersionType HeaderType => (VersionType) _version;
2019-07-28 23:04:23 +00:00
2019-09-14 04:35:42 +00:00
public ArchiveFlags ArchiveFlags => (ArchiveFlags) _archiveFlags;
2019-07-28 23:04:23 +00:00
2019-09-14 04:35:42 +00:00
public FileFlags FileFlags => (FileFlags) _archiveFlags;
2019-07-28 23:04:23 +00:00
2019-09-14 04:35:42 +00:00
public bool HasFolderNames => (_archiveFlags & 0x1) > 0;
2019-07-28 23:04:23 +00:00
2019-09-14 04:35:42 +00:00
public bool HasFileNames => (_archiveFlags & 0x2) > 0;
public bool CompressedByDefault => (_archiveFlags & 0x4) > 0;
public bool Bit9Set => (_archiveFlags & 0x100) > 0;
2019-07-28 23:04:23 +00:00
public bool HasNameBlobs
{
get
{
2019-09-14 04:35:42 +00:00
if (HeaderType == VersionType.FO3 || HeaderType == VersionType.SSE) return (_archiveFlags & 0x100) > 0;
2019-07-28 23:04:23 +00:00
return false;
}
}
public void Dispose()
{
_stream.Close();
}
private void LoadHeaders()
{
var fourcc = Encoding.ASCII.GetString(_rdr.ReadBytes(4));
if (fourcc != "BSA\0")
throw new InvalidDataException("Archive is not a BSA");
_magic = fourcc;
_version = _rdr.ReadUInt32();
_folderRecordOffset = _rdr.ReadUInt32();
_archiveFlags = _rdr.ReadUInt32();
_folderCount = _rdr.ReadUInt32();
_fileCount = _rdr.ReadUInt32();
_totalFolderNameLength = _rdr.ReadUInt32();
_totalFileNameLength = _rdr.ReadUInt32();
_fileFlags = _rdr.ReadUInt32();
LoadFolderRecords();
}
private void LoadFolderRecords()
{
_folders = new List<FolderRecord>();
2019-09-14 04:35:42 +00:00
for (var idx = 0; idx < _folderCount; idx += 1)
2019-07-28 23:04:23 +00:00
_folders.Add(new FolderRecord(this, _rdr));
foreach (var folder in _folders)
folder.LoadFileRecordBlock(this, _rdr);
foreach (var folder in _folders)
2019-09-14 04:35:42 +00:00
foreach (var file in folder._files)
file.LoadFileRecord(this, folder, file, _rdr);
2019-07-28 23:04:23 +00:00
}
}
public class BSAStateObject : ArchiveStateObject
{
public BSAStateObject() { }
public BSAStateObject(BSAReader bsaReader)
{
Magic = bsaReader._magic;
Version = bsaReader._version;
ArchiveFlags = bsaReader._archiveFlags;
FileFlags = bsaReader._fileFlags;
}
2020-03-09 20:38:35 +00:00
public override IBSABuilder MakeBuilder(long size)
{
2020-03-09 20:38:35 +00:00
return new BSABuilder(this, size);
}
public string Magic { get; set; }
public uint Version { get; set; }
public uint ArchiveFlags { get; set; }
public uint FileFlags { get; set; }
}
2019-07-28 23:04:23 +00:00
public class FolderRecord
{
2019-09-14 04:35:42 +00:00
private readonly uint _fileCount;
2019-07-28 23:04:23 +00:00
internal List<FileRecord> _files;
2019-09-14 04:35:42 +00:00
private ulong _offset;
private uint _unk;
2019-07-28 23:04:23 +00:00
internal FolderRecord(BSAReader bsa, BinaryReader src)
{
2019-09-14 04:35:42 +00:00
Hash = src.ReadUInt64();
2019-07-28 23:04:23 +00:00
_fileCount = src.ReadUInt32();
if (bsa.HeaderType == VersionType.SSE)
{
_unk = src.ReadUInt32();
_offset = src.ReadUInt64();
}
else
{
_offset = src.ReadUInt32();
}
}
public string Name { get; private set; }
2019-09-14 04:35:42 +00:00
public ulong Hash { get; }
2019-07-28 23:04:23 +00:00
internal void LoadFileRecordBlock(BSAReader bsa, BinaryReader src)
{
2019-09-14 04:35:42 +00:00
if (bsa.HasFolderNames) Name = src.ReadStringLen(bsa.HeaderType);
2019-07-28 23:04:23 +00:00
_files = new List<FileRecord>();
for (var idx = 0; idx < _fileCount; idx += 1)
_files.Add(new FileRecord(bsa, this, src, idx));
2019-07-28 23:04:23 +00:00
}
}
public class FileRecord : IFile
2019-07-28 23:04:23 +00:00
{
2019-09-14 04:35:42 +00:00
private readonly BSAReader _bsa;
private readonly long _dataOffset;
private readonly uint _dataSize;
2019-07-28 23:04:23 +00:00
private string _name;
2019-09-14 04:35:42 +00:00
private readonly string _nameBlob;
private readonly uint _offset;
private readonly uint _onDiskSize;
private readonly uint _originalSize;
private readonly uint _size;
internal readonly int _index;
2019-07-28 23:04:23 +00:00
public FileRecord(BSAReader bsa, FolderRecord folderRecord, BinaryReader src, int index)
2019-07-28 23:04:23 +00:00
{
_index = index;
2019-07-28 23:04:23 +00:00
_bsa = bsa;
2019-09-14 04:35:42 +00:00
Hash = src.ReadUInt64();
2019-09-08 22:44:15 +00:00
var size = src.ReadUInt32();
2019-09-14 04:35:42 +00:00
FlipCompression = (size & (0x1 << 30)) > 0;
2019-07-28 23:04:23 +00:00
2019-09-14 04:35:42 +00:00
if (FlipCompression)
2019-07-28 23:04:23 +00:00
_size = size ^ (0x1 << 30);
else
_size = size;
if (Compressed)
_size -= 4;
2019-09-08 03:34:18 +00:00
_offset = src.ReadUInt32();
2019-09-14 04:35:42 +00:00
Folder = folderRecord;
var old_pos = src.BaseStream.Position;
src.BaseStream.Position = _offset;
if (bsa.HasNameBlobs)
2019-09-08 03:34:18 +00:00
_nameBlob = src.ReadStringLenNoTerm(bsa.HeaderType);
2019-09-14 04:35:42 +00:00
if (Compressed)
_originalSize = src.ReadUInt32();
2019-09-14 04:35:42 +00:00
_onDiskSize = (uint) (_size - (_nameBlob == null ? 0 : _nameBlob.Length + 1));
if (Compressed)
{
_dataSize = _originalSize;
_onDiskSize -= 4;
}
else
2019-09-14 04:35:42 +00:00
{
_dataSize = _onDiskSize;
2019-09-14 04:35:42 +00:00
}
_dataOffset = src.BaseStream.Position;
src.BaseStream.Position = old_pos;
2019-07-28 23:04:23 +00:00
}
2020-03-23 12:57:18 +00:00
public RelativePath Path
2019-07-28 23:04:23 +00:00
{
get
{
2020-03-23 12:57:18 +00:00
return string.IsNullOrEmpty(Folder.Name) ? new RelativePath(_name) : new RelativePath(Folder.Name + "\\" + _name);
2019-07-28 23:04:23 +00:00
}
}
public bool Compressed
{
get
{
2019-09-14 04:35:42 +00:00
if (FlipCompression) return !_bsa.CompressedByDefault;
2019-07-28 23:04:23 +00:00
return _bsa.CompressedByDefault;
}
}
public uint Size => _dataSize;
public FileStateObject State => new BSAFileStateObject(this);
2019-09-14 04:35:42 +00:00
public ulong Hash { get; }
public FolderRecord Folder { get; }
public bool FlipCompression { get; }
2019-09-14 04:35:42 +00:00
internal void LoadFileRecord(BSAReader bsaReader, FolderRecord folder, FileRecord file, BinaryReader rdr)
{
2019-09-14 04:35:42 +00:00
_name = rdr.ReadStringTerm(_bsa.HeaderType);
}
2019-11-16 00:01:37 +00:00
public void CopyDataTo(Stream output)
2019-07-28 23:04:23 +00:00
{
2020-03-23 12:57:18 +00:00
using (var in_file = _bsa._fileName.OpenRead())
2019-07-28 23:04:23 +00:00
using (var rdr = new BinaryReader(in_file))
{
rdr.BaseStream.Position = _dataOffset;
if (_bsa.HeaderType == VersionType.SSE)
2019-07-28 23:04:23 +00:00
{
if (Compressed)
2019-07-28 23:04:23 +00:00
{
var r = LZ4Stream.Decode(rdr.BaseStream);
2019-11-16 00:01:37 +00:00
r.CopyToLimit(output, (int) _originalSize);
2019-07-28 23:04:23 +00:00
}
else
{
2019-11-16 00:01:37 +00:00
rdr.BaseStream.CopyToLimit(output, (int) _onDiskSize);
2019-07-28 23:04:23 +00:00
}
}
else
{
if (Compressed)
using (var z = new InflaterInputStream(rdr.BaseStream))
2019-09-14 04:35:42 +00:00
{
2019-11-16 00:01:37 +00:00
z.CopyToLimit(output, (int) _originalSize);
2019-09-14 04:35:42 +00:00
}
else
2019-11-16 00:01:37 +00:00
rdr.BaseStream.CopyToLimit(output, (int) _onDiskSize);
}
2019-07-28 23:04:23 +00:00
}
}
2019-11-16 00:01:37 +00:00
public byte[] GetData()
{
var ms = new MemoryStream();
2019-11-16 00:01:37 +00:00
CopyDataTo(ms);
return ms.ToArray();
}
2019-07-28 23:04:23 +00:00
}
public class BSAFileStateObject : FileStateObject
{
public BSAFileStateObject() { }
public BSAFileStateObject(FileRecord fileRecord)
{
FlipCompression = fileRecord.FlipCompression;
Path = fileRecord.Path;
Index = fileRecord._index;
}
public bool FlipCompression { get; set; }
}
}