use std::collections::HashSet;
use include_dir::{include_dir, Dir};
use super::unity_version::{UnityVersion, UnityVersionRange};
static UNITY_HEADERS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets/UnityHeaders");
static IL2CPP_API_HEADERS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets/Il2CppAPIHeaders");
#[derive(Debug, Clone)]
pub struct UnityResource {
pub name: String,
pub version_range: UnityVersionRange,
pub metadata_version: String,
pub is_api: bool,
}
impl UnityResource {
pub fn get_text(&self) -> Option<String> {
let dir = if self.is_api { &IL2CPP_API_HEADERS_DIR } else { &UNITY_HEADERS_DIR };
if let Some(file) = dir.get_file(&self.name) {
return file.contents_utf8().map(|s| s.to_string());
}
None
}
}
pub struct UnityHeaders {
pub metadata_version: String,
pub version_range: UnityVersionRange,
pub type_header: UnityResource,
pub api_header: UnityResource,
}
impl UnityHeaders {
pub fn new(type_header: UnityResource, api_header: UnityResource) -> Self {
let version_range = type_header.version_range.intersect(&api_header.version_range)
.unwrap_or_else(|| UnityVersionRange::new(type_header.version_range.min.clone(), type_header.version_range.max.clone()));
let metadata_version = type_header.metadata_version.clone();
Self {
metadata_version,
version_range,
type_header,
api_header,
}
}
pub fn get_type_header_text(&self, is_32bit: bool) -> String {
let mut str = if is_32bit { "#define IS_32BIT\n".to_string() } else { "".to_string() };
str.push_str(&self.type_header.get_text().unwrap_or_default());
let v5_3_6 = "5.3.6".parse::<UnityVersion>().unwrap();
let v5_4_6 = "5.4.6".parse::<UnityVersion>().unwrap();
if self.version_range.min >= v5_3_6 && self.version_range.max.as_ref().map_or(true, |m| *m <= v5_4_6) {
str.push_str("\nstruct VirtualInvokeData\n{\n Il2CppMethodPointer methodPtr;\n const MethodInfo* method;\n};\n");
}
str
}
pub fn get_api_header_text(&self) -> String {
self.api_header.get_text().unwrap_or_default()
}
pub fn get_all_type_headers() -> Vec<UnityResource> {
let mut resources = Vec::new();
for file in UNITY_HEADERS_DIR.files() {
let name = file.path().to_string_lossy().into_owned();
if name.ends_with(".h") {
let version_range = UnityVersionRange::from_filename(&name);
let metadata_version = name.split('-').next().unwrap_or("").to_string();
resources.push(UnityResource {
name,
version_range,
metadata_version,
is_api: false,
});
}
}
resources
}
pub fn get_all_api_headers() -> Vec<UnityResource> {
let mut resources = Vec::new();
for file in IL2CPP_API_HEADERS_DIR.files() {
let name = file.path().to_string_lossy().into_owned();
if name.ends_with(".h") {
let version_range = UnityVersionRange::from_filename(&name);
let metadata_version = name.split('-').next().unwrap_or("").to_string();
resources.push(UnityResource {
name,
version_range,
metadata_version,
is_api: true,
});
}
}
resources
}
fn get_function_names_from_api_header(text: &str) -> HashSet<String> {
let mut names = HashSet::new();
for line in text.lines() {
let line = line.trim();
if line.starts_with("DO_API") {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() >= 2 {
let name = parts[1].trim();
names.insert(name.to_string());
}
}
}
names
}
pub fn guess_headers_for_binary(
metadata_version: f32,
_is_32bit: bool,
api_exports: &HashSet<String>,
binary_field_offsets_are_pointers: bool,
) -> Vec<UnityHeaders> {
let mut type_headers: Vec<UnityResource> = Self::get_all_type_headers()
.into_iter()
.filter(|r| {
let Ok(mv) = r.metadata_version.parse::<f32>() else { return false; };
if (mv.floor() - metadata_version.floor()).abs() > 0.01 {
return false;
}
if (metadata_version - 21.0).abs() < 0.01 {
let v5_3_7 = "5.3.7".parse::<UnityVersion>().unwrap();
let v5_4_0 = "5.4.0".parse::<UnityVersion>().unwrap();
let header_field_offsets_are_pointers = r.version_range.min >= v5_3_7 && r.version_range.min != v5_4_0;
if header_field_offsets_are_pointers != binary_field_offsets_are_pointers {
return false;
}
}
true
}).collect();
type_headers.sort_by(|a, b| a.version_range.min.cmp(&b.version_range.min));
if type_headers.is_empty() { return vec![]; }
let total_range = UnityVersionRange::new(
type_headers.first().unwrap().version_range.min.clone(),
type_headers.last().unwrap().version_range.max.clone(),
);
let apis: Vec<UnityResource> = Self::get_all_api_headers()
.into_iter()
.filter(|a| a.version_range.intersect(&total_range).is_some())
.collect();
if apis.is_empty() { return vec![]; }
if api_exports.is_empty() {
println!("No IL2CPP API exports found in binary - IL2CPP APIs will be unavailable in C++ project");
return type_headers.into_iter().map(|t| {
let matching_api = apis.iter().rev().find(|a| a.version_range.intersect(&t.version_range).is_some()).unwrap_or(apis.last().unwrap());
UnityHeaders::new(t, matching_api.clone())
}).collect();
}
let mut api_matches = Vec::new();
for api in &apis {
if let Some(text) = api.get_text() {
let api_funcs = Self::get_function_names_from_api_header(&text);
let mut all_match = true;
for func in &api_funcs {
if !api_exports.contains(func) {
all_match = false;
break;
}
}
if all_match {
api_matches.push(api.clone());
}
}
}
if !api_matches.is_empty() {
println!("IL2CPP API discovery was successful");
let mut results = Vec::new();
for t in &type_headers {
for a in &api_matches {
if t.version_range.intersect(&a.version_range).is_some() {
results.push(UnityHeaders::new(t.clone(), a.clone()));
}
}
}
if !results.is_empty() {
return results;
}
}
println!("No exact match for IL2CPP APIs found in binary - IL2CPP API availability in C++ project will be partial");
type_headers.into_iter().map(|t| {
let matching_api = apis.iter().rev().find(|a| a.version_range.intersect(&t.version_range).is_some()).unwrap_or(apis.last().unwrap());
UnityHeaders::new(t, matching_api.clone())
}).collect()
}
}