/*
Copyright (C) 2019 erri120
This program is free software: you can redistribute it and/or modify
it 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.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
/*
* This file contains parts of the Oblivion Mod Manager licensed under GPLv2
* and has been modified for use in this OMODFramework
* Original source: https://www.nexusmods.com/oblivion/mods/2097
* GPLv2: https://opensource.org/licenses/gpl-2.0.php
*/
using System;
using System.Collections.Generic;
using System.IO;
using ICSharpCode.SharpZipLib.Checksum;
using ICSharpCode.SharpZipLib.Zip;
using Decoder = SevenZip.Compression.LZMA.Decoder;
namespace OMODExtraction
{
///
/// The Compression used for extraction and compression
///
public enum CompressionType : byte { SevenZip, Zip }
///
/// The level of compression used for extraction and compression
///
public enum CompressionLevel : byte { VeryHigh, High, Medium, Low, VeryLow, None }
internal class SparseFileWriterStream : Stream
{
private long _length;
private BinaryReader _fileList;
private string _currentFile;
private long _fileLength;
private long _written;
private FileStream _currentOutputStream;
internal string BaseDirectory
{
get;
}
internal SparseFileWriterStream(Stream fileList)
{
_fileList = new BinaryReader(fileList);
BaseDirectory = Utils.CreateTempDirectory();
CreateDirectoryStructure();
NextFile();
}
private void CreateDirectoryStructure()
{
long totalLength = 0;
while (_fileList.PeekChar() != -1)
{
string path = _fileList.ReadString();
_fileList.ReadInt32();
totalLength += _fileList.ReadInt64();
int upTo = 0;
while (true)
{
int i = path.IndexOf("\\", upTo, StringComparison.Ordinal);
if (i == -1) break;
string dir = path.Substring(0, i);
if (!Directory.Exists(Path.Combine(BaseDirectory, dir)))
Directory.CreateDirectory(Path.Combine(BaseDirectory, dir));
upTo = i + 1;
}
}
_length = totalLength;
_fileList.BaseStream.Position = 0;
}
private void NextFile()
{
_currentFile = _fileList.ReadString();
_fileList.ReadUInt32(); //CRC
_fileLength = _fileList.ReadInt64();
_currentOutputStream?.Close();
_currentOutputStream = File.Create(!Utils.IsSafeFileName(_currentFile)
? Path.Combine(Framework.TempDir, "IllegalFile")
: Path.Combine(BaseDirectory, _currentFile));
_written = 0;
}
public override long Position
{
get => 0;
set { throw new NotImplementedException("The SparseFileStream does not support seeking"); }
}
public override long Length => _length;
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException("The SparseFileStream does not support reading");
}
public override void Write(byte[] buffer, int offset, int count)
{
while (_written + count > _fileLength)
{
_currentOutputStream.Write(buffer, offset, (int)(_fileLength - _written));
offset += (int)(_fileLength - _written);
count -= (int)(_fileLength - _written);
NextFile();
}
if (count <= 0) return;
_currentOutputStream.Write(buffer, offset, count);
_written += count;
}
public override void SetLength(long length)
{
throw new NotImplementedException("The SparseFileStream does not support length");
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException("The SparseFileStream does not support seeking");
}
public override void Flush()
{
_currentOutputStream?.Flush();
}
public override void Close()
{
Flush();
while (_fileList.BaseStream.Position < _fileList.BaseStream.Length)
{
_currentFile = _fileList.ReadString();
_fileList.ReadUInt32(); //CRC
_fileLength = _fileList.ReadInt64();
if (_fileLength > 0)
throw new Framework.OMODFrameworkException(
"Compressed data file stream didn't contain enough information to fill all files");
_currentOutputStream?.Close();
_currentOutputStream = File.Create(!Utils.IsSafeFileName(_currentFile)
? Path.Combine(Framework.TempDir, "IllegalFile")
: Path.Combine(BaseDirectory, _currentFile));
}
_fileList?.Close();
_fileList = null;
if (_currentOutputStream == null) return;
_currentOutputStream.Close();
_currentOutputStream = null;
}
}
internal abstract class CompressionHandler
{
private static readonly Crc32 CRC32 = new Crc32();
private static readonly SevenZipHandler SevenZip = new SevenZipHandler();
private static readonly ZipHandler Zip = new ZipHandler();
internal static string DecompressFiles(Stream fileList, Stream compressedStream, CompressionType type)
{
switch (type)
{
case CompressionType.SevenZip: return SevenZip.DecompressAll(fileList, compressedStream);
case CompressionType.Zip: return Zip.DecompressAll(fileList, compressedStream);
default: throw new Framework.OMODFrameworkException("Unrecognized compression type.");
}
}
private static Stream GenerateFileList(IReadOnlyList files, IReadOnlyList folderStructure)
{
var fileList = new MemoryStream();
var bw = new BinaryWriter(fileList);
for (int i = 0; i < files.Count; i++)
{
bw.Write(folderStructure[i]);
bw.Write(CRC(files[i]));
bw.Write(new FileInfo(files[i]).Length);
}
bw.Flush();
fileList.Position = 0;
return fileList;
}
internal static uint CRC(string s)
{
var fs = File.OpenRead(s);
uint i = CRC(fs);
fs.Close();
return i;
}
internal static uint CRC(Stream inputStream)
{
byte[] buffer = new byte[4096];
CRC32.Reset();
while (inputStream.Position + 4096 < inputStream.Length)
{
inputStream.Read(buffer, 0, 4096);
CRC32.Update(buffer);
}
if (inputStream.Position >= inputStream.Length)
return (uint)CRC32.Value;
int i = (int)(inputStream.Length - inputStream.Position);
inputStream.Read(buffer, 0, i);
CRC32.Update(buffer);
return (uint)CRC32.Value;
}
internal static uint CRC(byte[] b)
{
CRC32.Reset();
CRC32.Update(b);
return (uint)CRC32.Value;
}
public static void WriteStreamToZip(BinaryWriter bw, Stream input)
{
input.Position = 0;
byte[] buffer = new byte[4096];
int upTo = 0;
while (input.Length - upTo > 4096)
{
input.Read(buffer, 0, 4096);
bw.Write(buffer, 0, 4096);
upTo += 4096;
}
if (input.Length - upTo <= 0)
return;
input.Read(buffer, 0, (int)(input.Length - upTo));
bw.Write(buffer, 0, (int)(input.Length - upTo));
}
protected abstract string DecompressAll(Stream fileList, Stream compressedStream);
}
internal class SevenZipHandler : CompressionHandler
{
protected override string DecompressAll(Stream fileList, Stream compressedStream)
{
var sfs = new SparseFileWriterStream(fileList);
byte[] buffer = new byte[5];
var decoder = new Decoder();
compressedStream.Read(buffer, 0, 5);
decoder.SetDecoderProperties(buffer);
try
{
decoder.Code(compressedStream, sfs, compressedStream.Length - compressedStream.Position, sfs.Length,
null);
}
finally
{
sfs.Close();
}
return sfs.BaseDirectory;
}
}
internal class ZipHandler : CompressionHandler
{
internal static int GetCompressionLevel(CompressionLevel level)
{
switch (level)
{
case CompressionLevel.VeryHigh:
return 9;
case CompressionLevel.High:
return 7;
case CompressionLevel.Medium:
return 5;
case CompressionLevel.Low:
return 3;
case CompressionLevel.VeryLow:
return 1;
case CompressionLevel.None:
return 0;
default:
throw new Framework.OMODFrameworkException("Unrecognized compression level.");
}
}
protected override string DecompressAll(Stream fileList, Stream compressedStream)
{
var sfs = new SparseFileWriterStream(fileList);
using (var zip = new ZipFile(compressedStream))
{
var file = zip.GetInputStream(0);
byte[] buffer = new byte[4096];
int i;
while ((i = file.Read(buffer, 0, 4096)) > 0)
{
sfs.Write(buffer, 0, i);
}
sfs.Close();
}
return sfs.BaseDirectory;
}
}
}