1 #region Copyright & License Information 2 /* 3 * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) 4 * This file is part of OpenRA, which is free software. It is made 5 * available to you under the terms of the GNU General Public License 6 * as published by the Free Software Foundation, either version 3 of 7 * the License, or (at your option) any later version. For more 8 * information, see COPYING. 9 */ 10 #endregion 11 12 using System; 13 using System.Collections.Generic; 14 using System.IO; 15 using System.Linq; 16 using ICSharpCode.SharpZipLib.Zip.Compression; 17 using ICSharpCode.SharpZipLib.Zip.Compression.Streams; 18 19 namespace OpenRA.Mods.Common.FileFormats 20 { 21 public sealed class MSCabCompression 22 { 23 class CabFolder 24 { 25 public readonly uint BlockOffset; 26 public readonly ushort BlockCount; 27 public readonly ushort CompressionType; 28 CabFolder(Stream stream)29 public CabFolder(Stream stream) 30 { 31 BlockOffset = stream.ReadUInt32(); 32 BlockCount = stream.ReadUInt16(); 33 CompressionType = stream.ReadUInt16(); 34 } 35 } 36 37 class CabFile 38 { 39 public readonly string FileName; 40 public readonly uint DecompressedLength; 41 public readonly uint DecompressedOffset; 42 public readonly ushort FolderIndex; 43 CabFile(Stream stream)44 public CabFile(Stream stream) 45 { 46 DecompressedLength = stream.ReadUInt32(); 47 DecompressedOffset = stream.ReadUInt32(); 48 FolderIndex = stream.ReadUInt16(); 49 stream.Position += 6; 50 FileName = stream.ReadASCIIZ(); 51 } 52 } 53 54 readonly CabFolder[] folders; 55 readonly CabFile[] files; 56 readonly Stream stream; 57 MSCabCompression(Stream stream)58 public MSCabCompression(Stream stream) 59 { 60 this.stream = stream; 61 62 var signature = stream.ReadASCII(4); 63 if (signature != "MSCF") 64 throw new InvalidDataException("Not a Microsoft CAB package!"); 65 66 stream.Position += 12; 67 var filesOffset = stream.ReadUInt32(); 68 stream.Position += 6; 69 var folderCount = stream.ReadUInt16(); 70 var fileCount = stream.ReadUInt16(); 71 if (stream.ReadUInt16() != 0) 72 throw new InvalidDataException("Only plain packages (without reserved header space or prev/next archives) are supported!"); 73 74 stream.Position += 4; 75 76 folders = new CabFolder[folderCount]; 77 for (var i = 0; i < folderCount; i++) 78 { 79 folders[i] = new CabFolder(stream); 80 if (folders[i].CompressionType != 1) 81 throw new InvalidDataException("Compression type is not supported"); 82 } 83 84 files = new CabFile[fileCount]; 85 stream.Seek(filesOffset, SeekOrigin.Begin); 86 for (var i = 0; i < fileCount; i++) 87 files[i] = new CabFile(stream); 88 } 89 ExtractFile(string filename, Stream output, Action<int> onProgress = null)90 public void ExtractFile(string filename, Stream output, Action<int> onProgress = null) 91 { 92 var file = files.FirstOrDefault(f => f.FileName == filename); 93 if (file == null) 94 throw new FileNotFoundException(filename); 95 96 var folder = folders[file.FolderIndex]; 97 stream.Seek(folder.BlockOffset, SeekOrigin.Begin); 98 99 var inflater = new Inflater(true); 100 var buffer = new byte[4096]; 101 var decompressedBytes = 0; 102 for (var i = 0; i < folder.BlockCount; i++) 103 { 104 if (onProgress != null) 105 onProgress((int)(100 * output.Position / file.DecompressedLength)); 106 107 // Ignore checksums 108 stream.Position += 4; 109 var blockLength = stream.ReadUInt16(); 110 stream.Position += 4; 111 112 using (var batch = new MemoryStream(stream.ReadBytes(blockLength - 2))) 113 using (var inflaterStream = new InflaterInputStream(batch, inflater)) 114 { 115 int n; 116 while ((n = inflaterStream.Read(buffer, 0, buffer.Length)) > 0) 117 { 118 var offset = Math.Max(0, file.DecompressedOffset - decompressedBytes); 119 var count = Math.Min(n - offset, file.DecompressedLength - decompressedBytes); 120 if (offset < n) 121 output.Write(buffer, (int)offset, (int)count); 122 123 decompressedBytes += n; 124 } 125 } 126 127 inflater.Reset(); 128 } 129 } 130 131 public IEnumerable<string> Contents { get { return files.Select(f => f.FileName); } } 132 } 133 } 134