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