Files
Il2CppInspectorRedux/Il2CppInspector.Common/Cpp/UnityHeaders/UnityVersion.cs

239 lines
9.9 KiB
C#

/*
Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
Copyright 2020 Robert Xiao - https://robertxiao.ca
Copyright 2023 LukeFZ - https://github.com/LukeFZ
All rights reserved.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace Il2CppInspector.Cpp.UnityHeaders
{
// Parsed representation of a Unity version number, such as 5.3.0f1 or 2019.3.7.
public partial class UnityVersion : IComparable<UnityVersion>, IEquatable<UnityVersion>
{
// A sorted enumeration of build types, in order of maturity
public enum BuildTypeEnum
{
Unspecified,
Alpha,
Beta,
ReleaseCandidate,
Final,
Patch,
}
public static string BuildTypeToString(BuildTypeEnum buildType) => buildType switch
{
BuildTypeEnum.Unspecified => "",
BuildTypeEnum.Alpha => "a",
BuildTypeEnum.Beta => "b",
BuildTypeEnum.ReleaseCandidate => "rc",
BuildTypeEnum.Final => "f",
BuildTypeEnum.Patch => "p",
_ => throw new ArgumentException(),
};
public static BuildTypeEnum StringToBuildType(string s) => s switch
{
"" => BuildTypeEnum.Unspecified,
"a" => BuildTypeEnum.Alpha,
"b" => BuildTypeEnum.Beta,
"rc" => BuildTypeEnum.ReleaseCandidate,
"f" => BuildTypeEnum.Final,
"p" => BuildTypeEnum.Patch,
_ => throw new ArgumentException("Unknown build type " + s),
};
// Unity version number is of the form <Major>.<Minor>.<Update>[<BuildType><BuildNumber>]
public int Major { get; }
public int Minor { get; }
public int Update { get; }
public BuildTypeEnum BuildType { get; }
public int BuildNumber { get; }
public UnityVersion(string versionString) {
var match = VersionRegex().Match(versionString);
if (!match.Success)
throw new ArgumentException($"'${versionString}' is not a valid Unity version number.");
Major = int.Parse(match.Groups[1].Value);
Minor = int.Parse(match.Groups[2].Value);
Update = match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0;
BuildType = match.Groups[4].Success ? StringToBuildType(match.Groups[4].Value) : BuildTypeEnum.Unspecified;
BuildNumber = match.Groups[5].Success ? int.Parse(match.Groups[5].Value) : 0;
}
// Get a Unity version from a Unity asset file
public static UnityVersion FromAssetFile(string filePath) {
// Don't use BinaryObjectStream because we'd have to read the entire file into memory
using var file = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new BinaryReader(file, System.Text.Encoding.UTF8);
// Position of Unity version string in asset files
file.Position = 0x14;
// Read null-terminated string
var bytes = new List<byte>();
var maxLength = 15;
byte b;
while ((b = reader.ReadByte()) != 0 && bytes.Count < maxLength)
bytes.Add(b);
var unityString = System.Text.Encoding.UTF8.GetString(bytes.ToArray());
return new UnityVersion(unityString);
}
public static implicit operator UnityVersion(string versionString) => new(versionString);
public override string ToString() {
var res = $"{Major}.{Minor}.{Update}";
if (BuildType != BuildTypeEnum.Unspecified)
res += $"{BuildTypeToString(BuildType)}{BuildNumber}";
return res;
}
// Compare two version numbers, intransitively (due to the Unspecified build type)
public int CompareTo(UnityVersion other) {
// null means maximum possible version
if (other == null)
return -1;
int res;
if (0 != (res = Major.CompareTo(other.Major)))
return res;
if (0 != (res = Minor.CompareTo(other.Minor)))
return res;
if (0 != (res = Update.CompareTo(other.Update)))
return res;
// same major.minor.update - if one of these is suffix-less, they compare equal
// yes, this makes the compare function non-transitive; don't use it to sort things
if (BuildType == BuildTypeEnum.Unspecified || other.BuildType == BuildTypeEnum.Unspecified)
return 0;
if (0 != (res = BuildType.CompareTo(other.BuildType)))
return res;
if (0 != (res = BuildNumber.CompareTo(other.BuildNumber)))
return res;
return 0;
}
// Equality comparisons
public static bool operator ==(UnityVersion first, UnityVersion second) {
if (ReferenceEquals(first, second))
return true;
if (ReferenceEquals(first, null) || ReferenceEquals(second, null))
return false;
return first.Equals(second);
}
public static bool operator !=(UnityVersion first, UnityVersion second) => !(first == second);
public override bool Equals(object obj) => Equals(obj as UnityVersion);
public bool Equals(UnityVersion other) {
return other != null &&
Major == other.Major &&
Minor == other.Minor &&
Update == other.Update &&
BuildType == other.BuildType &&
BuildNumber == other.BuildNumber;
}
public override int GetHashCode() => HashCode.Combine(Major, Minor, Update, BuildType, BuildNumber);
[GeneratedRegex(@"^(\d+)\.(\d+)(?:\.(\d+))?(?:([a-zA-Z]+)(\d+))?$")]
private static partial Regex VersionRegex();
}
// A range of Unity versions
public class UnityVersionRange : IComparable<UnityVersionRange>, IEquatable<UnityVersionRange>
{
// Minimum and maximum Unity version numbers for this range. Both endpoints are inclusive
// Max can be null to specify no upper bound
public UnityVersion Min { get; }
public UnityVersion Max { get; }
// Determine if this range contains the specified version
public bool Contains(UnityVersion version) => version.CompareTo(Min) >= 0 && (Max == null || version.CompareTo(Max) <= 0);
public UnityVersionRange(UnityVersion min, UnityVersion max) {
Min = min;
Max = max;
}
// Create a version range from a string, in the format "[Il2CppInspector.Cpp.<namespace-leaf>.][metadataVersion-]<min>-[max].h"
public static UnityVersionRange FromFilename(string headerFilename) {
var baseNamespace = "Il2CppInspector.Cpp.";
headerFilename = headerFilename.Replace(".h", "");
if (headerFilename.StartsWith(baseNamespace)) {
headerFilename = headerFilename.Substring(baseNamespace.Length);
headerFilename = headerFilename.Substring(headerFilename.IndexOf(".") + 1);
}
var bits = headerFilename.Split("-");
// Metadata version supplied
// Note: This relies on the metadata version being either 2 or 4 characters,
// and that the smallest Unity version must be 5 characters or more
if (headerFilename[2] == '-' || headerFilename[4] == '-')
bits = bits.Skip(1).ToArray();
var Min = new UnityVersion(bits[0]);
UnityVersion Max = null;
if (bits.Length == 1)
Max = Min;
if (bits.Length == 2 && bits[1] != "")
Max = new UnityVersion(bits[1]);
return new UnityVersionRange(Min, Max);
}
// Compare and sort based on the lowest version number
public int CompareTo(UnityVersionRange other) => Min.CompareTo(other.Min);
// Intersect two ranges to find the smallest shared set of versions
// Returns null if the two ranges do not intersect
// Max == null means no upper bound on version
public UnityVersionRange Intersect(UnityVersionRange other) {
var highestLow = Min.CompareTo(other.Min) > 0 ? Min : other.Min;
var lowestHigh = Max == null? other.Max : Max.CompareTo(other.Max) < 0 ? Max : other.Max;
if (highestLow.CompareTo(lowestHigh) > 0)
return null;
return new UnityVersionRange(highestLow, lowestHigh);
}
public override string ToString() {
var res = $"{Min}";
if (Max == null)
res += "+";
else if (!Max.Equals(Min))
res += $" - {Max}";
return res;
}
// Equality comparisons
public static bool operator ==(UnityVersionRange first, UnityVersionRange second) {
if (ReferenceEquals(first, second))
return true;
if (ReferenceEquals(first, null) || ReferenceEquals(second, null))
return false;
return first.Equals(second);
}
public static bool operator !=(UnityVersionRange first, UnityVersionRange second) => !(first == second);
public override bool Equals(object obj) => Equals(obj as UnityVersionRange);
public bool Equals(UnityVersionRange other) => Min.Equals(other?.Min)
&& ((Max != null && Max.Equals(other?.Max))
|| (Max == null && other != null && other.Max == null));
public override int GetHashCode() => HashCode.Combine(Min, Max);
}
}