Skip to content

Instantly share code, notes, and snippets.

@airbreather airbreather/STEPOptions.cs
Last active Sep 19, 2016

Embed
What would you like to do?
using CommandLine;
using CommandLine.Text;
namespace StepperUpper
{
internal sealed class Options
{
// input file
[Option('p', "packDefinitionFile", Required = true, HelpText = "The .xml file that defines a pack.")]
public string PackDefinitionFilePath { get; set; }
// main input directory
[Option('d', "downloadFolder", Required = true, HelpText = "Folder containing downloaded mod files.")]
public string DownloadDirectoryPath { get; set; }
// secondary directory for both input and output
[Option('s', "steamFolder", Required = true, HelpText = "Folder containing \"steamapps\".")]
public string SteamDirectoryPath { get; set; }
// main output directory
[Option('m', "modOrganizerFolder", Required = true, HelpText = "Where Mod Organizer stores its data.")]
public string ModOrganizerDirectoryPath { get; set; }
[Option('x', "scorch", DefaultValue = false, HelpText = "True to erase existing files.")]
public bool Scorch { get; set; }
[ParserState]
public IParserState LastParserState { get; set; }
[HelpOption]
public string GetUsage() => HelpText.AutoBuild(this, current => HelpText.DefaultParsingErrorsHandler(this, current));
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace StepperUpper
{
internal static class Program
{
private static readonly Lazy<Regex> hexStringRegex = new Lazy<Regex>(() => new Regex("^([A-Fa-f0-9]{2})*$",
RegexOptions.CultureInvariant |
RegexOptions.Compiled |
RegexOptions.ExplicitCapture),
LazyThreadSafetyMode.PublicationOnly);
private static void Main(string[] args)
{
Options options = new Options();
CommandLine.Parser.Default.ParseArgumentsStrict(args, options);
IEnumerable<string> files = Directory.EnumerateFiles(options.DownloadDirectoryPath);
if (options.SteamDirectoryPath != null)
{
files = files.Concat(Directory.EnumerateFiles(Path.Combine(options.SteamDirectoryPath, "steamapps", "common", "Skyrim", "Data")));
}
Dictionary<MD5Checksum, string> checkedFiles = Task.WhenAll(
files.Select(
f => Task.Run(async () =>
{
using (FileStream fs = new FileStream(f, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan))
using (var md5 = MD5.Create())
{
byte[] buf = new byte[81920];
int cnt;
while ((cnt = await fs.ReadAsync(buf, 0, buf.Length).ConfigureAwait(false)) != 0)
{
md5.TransformBlock(buf, 0, cnt, null, 0);
}
md5.TransformFinalBlock(buf, 0, 0);
return new FileWithChecksum(f, new MD5Checksum(md5.Hash));
}
})))
.Result
.DistinctBy(f => f.Checksum)
.ToDictionary(f => f.Checksum, f => f.Path);
XElement[] missing = XDocument.Load(options.PackDefinitionFilePath)
.Element("Modpack")
.Element("Files")
.Elements("Group")
.SelectMany(grp => grp.Elements("File"))
.Where(fl => !MD5Checksums(fl).Any(ck => checkedFiles.ContainsKey(ck)))
.ToArray();
}
private static IEnumerable<MD5Checksum> MD5Checksums(XElement element)
{
yield return new MD5Checksum(element.Attribute("MD5Checksum").Value);
foreach (XElement alternateMD5Checksum in element.Elements("AlternateMD5Checksum"))
{
yield return new MD5Checksum(alternateMD5Checksum.Value);
}
}
#region Helper Extensions
private static IEnumerable<TSource> DistinctBy<TSource, TCompare>(this IEnumerable<TSource> enumerable, Func<TSource, TCompare> selector)
{
HashSet<TCompare> closedSet = new HashSet<TCompare>();
foreach (TSource value in enumerable)
{
if (closedSet.Add(selector(value)))
{
yield return value;
}
}
}
private static T ValidateNotNull<T>(this T value, string paramName) where T : class
{
if (value == null)
{
throw new ArgumentNullException(paramName);
}
return value;
}
private static byte[] HexStringToByteArrayChecked(this string s)
{
s.ValidateNotNull(nameof(s));
if (!hexStringRegex.Value.IsMatch(s))
{
throw new ArgumentException("Must provide a hex string.", nameof(s));
}
return s.HexStringToByteArrayUnchecked();
}
// http://stackoverflow.com/a/17923942/1083771
private static byte[] HexStringToByteArrayUnchecked(this string s)
{
byte[] bytes = new byte[s.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
int hi = s[i * 2] - 65;
hi = hi + 10 + ((hi >> 31) & 7);
int lo = s[i * 2 + 1] - 65;
lo = lo + 10 + ((lo >> 31) & 7) & 0x0f;
bytes[i] = (byte)(lo | hi << 4);
}
return bytes;
}
// http://stackoverflow.com/a/24343727/1083771
private static readonly uint[] byteToHex32Lookup = CreateLookup32Unsafe();
private static uint[] CreateLookup32Unsafe()
{
uint[] result = new uint[256];
for (int i = 0; i < result.Length; i++)
{
string s = i.ToString("x2");
result[i] = BitConverter.IsLittleEndian
? s[0] + ((uint)s[1] << 16)
: s[1] + ((uint)s[0] << 16);
}
return result;
}
private static unsafe string ToHexString(byte* bytes, int cnt)
{
string result = new string(default(char), cnt * 2);
fixed (uint* byteToHexPtr = byteToHex32Lookup)
fixed (char* resultCharPtr = result)
{
uint* resultUInt32Ptr = (uint*)resultCharPtr;
for (int i = 0; i < cnt; i++)
{
resultUInt32Ptr[i] = byteToHexPtr[bytes[i]];
}
}
return result;
}
#endregion
// Sequential guarantees that ToString() is correct.
[StructLayout(LayoutKind.Sequential)]
private struct MD5Checksum : IEquatable<MD5Checksum>
{
internal ulong X0;
internal ulong X1;
internal MD5Checksum(string s)
: this(s.HexStringToByteArrayChecked())
{
}
internal MD5Checksum(byte[] buf)
{
buf.ValidateNotNull(nameof(buf));
this.X0 = BitConverter.ToUInt64(buf, 0);
this.X1 = BitConverter.ToUInt64(buf, 8);
}
public bool Equals(MD5Checksum other) => this.X0 == other.X0 && this.X1 == other.X1;
public override bool Equals(object obj) => obj is MD5Checksum && this.Equals((MD5Checksum)obj);
public override int GetHashCode() => (this.X0 ^ this.X1).GetHashCode();
public unsafe override string ToString()
{
fixed (MD5Checksum* ptr = &this)
{
return ToHexString((byte*)ptr, 16);
}
}
}
[StructLayout(LayoutKind.Auto)]
private struct FileWithChecksum
{
internal MD5Checksum Checksum;
internal string Path;
internal FileWithChecksum(string path, MD5Checksum checksum)
{
this.Path = path;
this.Checksum = checksum;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.