Files
OpenRA/OpenRA.Mods.Common/FileFormats/MSCabCompression.cs
RoosterDragon e6914f707a Introduce FirstOrDefault extensions method for Array.Find and List.Find.
This allows the LINQ spelling to be used, but benefits from the performance improvement of the specific methods for these classes that provide the same result.
2023-11-19 19:28:57 +02:00

133 lines
3.7 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
namespace OpenRA.Mods.Common.FileFormats
{
public sealed class MSCabCompression
{
sealed class CabFolder
{
public readonly uint BlockOffset;
public readonly ushort BlockCount;
public readonly ushort CompressionType;
public CabFolder(Stream stream)
{
BlockOffset = stream.ReadUInt32();
BlockCount = stream.ReadUInt16();
CompressionType = stream.ReadUInt16();
}
}
sealed class CabFile
{
public readonly string FileName;
public readonly uint DecompressedLength;
public readonly uint DecompressedOffset;
public readonly ushort FolderIndex;
public CabFile(Stream stream)
{
DecompressedLength = stream.ReadUInt32();
DecompressedOffset = stream.ReadUInt32();
FolderIndex = stream.ReadUInt16();
stream.Position += 6;
FileName = stream.ReadASCIIZ();
}
}
readonly CabFolder[] folders;
readonly CabFile[] files;
readonly Stream stream;
public MSCabCompression(Stream stream)
{
this.stream = stream;
var signature = stream.ReadASCII(4);
if (signature != "MSCF")
throw new InvalidDataException("Not a Microsoft CAB package!");
stream.Position += 12;
var filesOffset = stream.ReadUInt32();
stream.Position += 6;
var folderCount = stream.ReadUInt16();
var fileCount = stream.ReadUInt16();
if (stream.ReadUInt16() != 0)
throw new InvalidDataException("Only plain packages (without reserved header space or prev/next archives) are supported!");
stream.Position += 4;
folders = new CabFolder[folderCount];
for (var i = 0; i < folderCount; i++)
{
folders[i] = new CabFolder(stream);
if (folders[i].CompressionType != 1)
throw new InvalidDataException("Compression type is not supported");
}
files = new CabFile[fileCount];
stream.Seek(filesOffset, SeekOrigin.Begin);
for (var i = 0; i < fileCount; i++)
files[i] = new CabFile(stream);
}
public void ExtractFile(string filename, Stream output, Action<int> onProgress = null)
{
var file = files.FirstOrDefault(f => f.FileName == filename);
if (file == null)
throw new FileNotFoundException(filename);
var folder = folders[file.FolderIndex];
stream.Seek(folder.BlockOffset, SeekOrigin.Begin);
var inflater = new Inflater(true);
var buffer = new byte[4096];
var decompressedBytes = 0;
for (var i = 0; i < folder.BlockCount; i++)
{
onProgress?.Invoke((int)(100 * output.Position / file.DecompressedLength));
// Ignore checksums
stream.Position += 4;
var blockLength = stream.ReadUInt16();
stream.Position += 4;
using (var batch = new MemoryStream(stream.ReadBytes(blockLength - 2)))
using (var inflaterStream = new InflaterInputStream(batch, inflater))
{
int n;
while ((n = inflaterStream.Read(buffer, 0, buffer.Length)) > 0)
{
var offset = Math.Max(0, file.DecompressedOffset - decompressedBytes);
var count = Math.Min(n - offset, file.DecompressedLength - decompressedBytes);
if (offset < n)
output.Write(buffer, (int)offset, (int)count);
decompressedBytes += n;
}
}
inflater.Reset();
}
}
public IEnumerable<string> Contents { get { return files.Select(f => f.FileName); } }
}
}