Last active
September 19, 2016 12:30
-
-
Save airbreather/5a6dbae2b7a593f71be07e2d750dba1d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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