From 43cbafc0f5ff3a3e20d6a27f0abde86b8439cbcf Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 26 Oct 2020 20:34:47 -0700 Subject: [PATCH] Add better detection for XCP2 --- BurnOutSharp/FileType/IniFile.cs | 296 +++++++++++++++++++++++++++++ BurnOutSharp/ProtectionType/XCP.cs | 33 +++- 2 files changed, 325 insertions(+), 4 deletions(-) create mode 100644 BurnOutSharp/FileType/IniFile.cs diff --git a/BurnOutSharp/FileType/IniFile.cs b/BurnOutSharp/FileType/IniFile.cs new file mode 100644 index 00000000..9afb335f --- /dev/null +++ b/BurnOutSharp/FileType/IniFile.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace BurnOutSharp.FileType +{ + public class IniFile : IDictionary + { + private Dictionary _keyValuePairs = new Dictionary(); + + public string this[string key] + { + get + { + if (_keyValuePairs == null) + _keyValuePairs = new Dictionary(); + + key = key.ToLowerInvariant(); + if (_keyValuePairs.ContainsKey(key)) + return _keyValuePairs[key]; + + return null; + } + set + { + if (_keyValuePairs == null) + _keyValuePairs = new Dictionary(); + + key = key.ToLowerInvariant(); + _keyValuePairs[key] = value; + } + } + + /// + /// Create an empty INI file + /// + public IniFile() + { + } + + /// + /// Populate an INI file from path + /// + public IniFile(string path) + { + this.Parse(path); + } + + /// + /// Populate an INI file from stream + /// + public IniFile(Stream stream) + { + this.Parse(stream); + } + + /// + /// Add or update a key and value to the INI file + /// + public void AddOrUpdate(string key, string value) + { + _keyValuePairs[key.ToLowerInvariant()] = value; + } + + /// + /// Remove a key from the INI file + /// + public void Remove(string key) + { + _keyValuePairs.Remove(key.ToLowerInvariant()); + } + + /// + /// Read an INI file based on the path + /// + public bool Parse(string path) + { + // If we don't have a file, we can't read it + if (!File.Exists(path)) + return false; + + using (var fileStream = File.OpenRead(path)) + { + return Parse(fileStream); + } + } + + /// + /// Read an INI file from a stream + /// + public bool Parse(Stream stream) + { + // If the stream is invalid or unreadable, we can't process it + if (stream == null || !stream.CanRead || stream.Position >= stream.Length - 1) + return false; + + // Keys are case-insensitive by default + try + { + using (StreamReader sr = new StreamReader(stream)) + { + string section = string.Empty; + while (!sr.EndOfStream) + { + string line = sr.ReadLine().Trim(); + + // Comments start with ';' + if (line.StartsWith(";")) + { + // No-op, we don't process comments + } + + // Section titles are surrounded by square brackets + else if (line.StartsWith("[")) + { + section = line.TrimStart('[').TrimEnd(']'); + } + + // Valid INI lines are in the format key=value + else if (line.Contains("=")) + { + // Split the line by '=' for key-value pairs + string[] data = line.Split('='); + + // If the value field contains an '=', we need to put them back in + string key = data[0].Trim(); + string value = string.Join("=", data.Skip(1)).Trim(); + + // Section names are prepended to the key with a '.' separating + if (!string.IsNullOrEmpty(section)) + key = $"{section}.{key}"; + + // Set or overwrite keys in the returned dictionary + _keyValuePairs[key.ToLowerInvariant()] = value; + } + + // All other lines are ignored + } + } + } + catch + { + // We don't care what the error was, just catch and return + return false; + } + + return true; + } + + /// + /// Write an INI file to a path + /// + public bool Write(string path) + { + // If we don't have a valid dictionary with values, we can't write out + if (_keyValuePairs == null || _keyValuePairs.Count == 0) + return false; + + using (var fileStream = File.OpenWrite(path)) + { + return Write(fileStream); + } + } + + /// + /// Write an INI file to a stream + /// + public bool Write(Stream stream) + { + // If we don't have a valid dictionary with values, we can't write out + if (_keyValuePairs == null || _keyValuePairs.Count == 0) + return false; + + // If the stream is invalid or unwritable, we can't output to it + if (stream == null || !stream.CanWrite || stream.Position >= stream.Length - 1) + return false; + + try + { + using (StreamWriter sw = new StreamWriter(stream)) + { + // Order the dictionary by keys to link sections together + var orderedKeyValuePairs = _keyValuePairs.OrderBy(kvp => kvp.Key); + + string section = string.Empty; + foreach (var keyValuePair in orderedKeyValuePairs) + { + // Extract the key and value + string key = keyValuePair.Key; + string value = keyValuePair.Value; + + // We assume '.' is a section name separator + if (key.Contains('.')) + { + // Split the key by '.' + string[] data = keyValuePair.Key.Split('.'); + + // If the key contains an '.', we need to put them back in + string newSection = data[0].Trim(); + key = string.Join(".", data.Skip(1)).Trim(); + + // If we have a new section, write it out + if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase)) + { + sw.WriteLine($"[{newSection}]"); + section = newSection; + } + } + + // Now write out the key and value in a standardized way + sw.WriteLine($"{key}={value}"); + } + } + } + catch + { + // We don't care what the error was, just catch and return + return false; + } + + return true; + } + + #region IDictionary Impelementations + + public ICollection Keys => ((IDictionary)_keyValuePairs).Keys; + + public ICollection Values => ((IDictionary)_keyValuePairs).Values; + + public int Count => ((ICollection>)_keyValuePairs).Count; + + public bool IsReadOnly => ((ICollection>)_keyValuePairs).IsReadOnly; + + public void Add(string key, string value) + { + ((IDictionary)_keyValuePairs).Add(key.ToLowerInvariant(), value); + } + + bool IDictionary.Remove(string key) + { + return ((IDictionary)_keyValuePairs).Remove(key.ToLowerInvariant()); + } + + public bool TryGetValue(string key, out string value) + { + return ((IDictionary)_keyValuePairs).TryGetValue(key.ToLowerInvariant(), out value); + } + + public void Add(KeyValuePair item) + { + var newItem = new KeyValuePair(item.Key.ToLowerInvariant(), item.Value); + ((ICollection>)_keyValuePairs).Add(newItem); + } + + public void Clear() + { + ((ICollection>)_keyValuePairs).Clear(); + } + + public bool Contains(KeyValuePair item) + { + var newItem = new KeyValuePair(item.Key.ToLowerInvariant(), item.Value); + return ((ICollection>)_keyValuePairs).Contains(newItem); + } + + public bool ContainsKey(string key) + { + return _keyValuePairs.ContainsKey(key.ToLowerInvariant()); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((ICollection>)_keyValuePairs).CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + var newItem = new KeyValuePair(item.Key.ToLowerInvariant(), item.Value); + return ((ICollection>)_keyValuePairs).Remove(newItem); + } + + public IEnumerator> GetEnumerator() + { + return ((IEnumerable>)_keyValuePairs).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_keyValuePairs).GetEnumerator(); + } + + #endregion + } +} diff --git a/BurnOutSharp/ProtectionType/XCP.cs b/BurnOutSharp/ProtectionType/XCP.cs index 2d2d8cd5..726e033f 100644 --- a/BurnOutSharp/ProtectionType/XCP.cs +++ b/BurnOutSharp/ProtectionType/XCP.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using BurnOutSharp.FileType; namespace BurnOutSharp.ProtectionType { @@ -11,9 +12,14 @@ namespace BurnOutSharp.ProtectionType { if (isDirectory) { + // INI-like file that can be parsed out + string xcpDatPath = files.FirstOrDefault(f => Path.GetFileName(f).Equals("VERSION.DAT", StringComparison.OrdinalIgnoreCase)) + ?? files.FirstOrDefault(f => Path.GetFileName(f).Equals("XCP.DAT", StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(xcpDatPath)) + return GetXCPVersion(xcpDatPath); + // TODO: Verify if these are OR or AND - if (files.Any(f => Path.GetFileName(f).Equals("XCP.DAT", StringComparison.OrdinalIgnoreCase)) - || files.Any(f => Path.GetFileName(f).Equals("ECDPlayerControl.ocx", StringComparison.OrdinalIgnoreCase)) + if (files.Any(f => Path.GetFileName(f).Equals("ECDPlayerControl.ocx", StringComparison.OrdinalIgnoreCase)) || files.Any(f => Path.GetFileName(f).Equals("go.exe", StringComparison.OrdinalIgnoreCase))) // Path.Combine("contents", "go.exe") { return "XCP"; @@ -21,8 +27,14 @@ namespace BurnOutSharp.ProtectionType } else { - if (Path.GetFileName(path).Equals("XCP.DAT", StringComparison.OrdinalIgnoreCase) - || Path.GetFileName(path).Equals("ECDPlayerControl.ocx", StringComparison.OrdinalIgnoreCase) + // INI-like file that can be parsed out + if (Path.GetFileName(path).Equals("VERSION.DAT", StringComparison.OrdinalIgnoreCase) + || Path.GetFileName(path).Equals("XCP.DAT", StringComparison.OrdinalIgnoreCase)) + { + return GetXCPVersion(path); + } + + if (Path.GetFileName(path).Equals("ECDPlayerControl.ocx", StringComparison.OrdinalIgnoreCase) || Path.GetFileName(path).Equals("go.exe", StringComparison.OrdinalIgnoreCase)) { return "XCP"; @@ -31,5 +43,18 @@ namespace BurnOutSharp.ProtectionType return null; } + + private static string GetXCPVersion(string path) + { + try + { + var xcpIni = new IniFile(path); + return xcpIni["XCP.VERSION"]; + } + catch + { + return null; + } + } } }