Skip to main content

il2cpp_bridge_rs/api/debugging/
cs.rs

1//! Metadata dump helpers that emit C#-like pseudo-code.
2use crate::api::{cache, Application};
3#[cfg(dev_release)]
4use crate::logger;
5use crate::structs::Assembly;
6use std::fs::File;
7use std::io::{BufWriter, Write};
8use std::path::{Path, PathBuf};
9
10/// Writes a single assembly to the provided writer
11fn write_assembly(writer: &mut dyn Write, assembly: &Assembly) -> std::io::Result<()> {
12    if !assembly.classes.is_empty() {
13        for class in &assembly.classes {
14            writeln!(writer, "\n{}", class)?;
15        }
16    }
17    Ok(())
18}
19
20/// Collects all assemblies from the cache, sorted by their minimum class token
21/// so the order matches the IL2CPP metadata image table.
22fn sorted_assemblies() -> Vec<std::sync::Arc<Assembly>> {
23    let mut assemblies: Vec<_> = cache::CACHE
24        .assemblies
25        .iter()
26        .map(|e| std::sync::Arc::clone(e.value()))
27        .collect();
28
29    assemblies.sort_by_key(|a| {
30        a.classes
31            .iter()
32            .map(|c| c.token & 0x00FF_FFFF)
33            .min()
34            .unwrap_or(u32::MAX)
35    });
36
37    assemblies
38}
39
40/// Writes the `// Image N: foo.dll - startIndex` header block to any writer.
41/// The starting TypeDefIndex for each image is `min_token - 1` (tokens are 1-based).
42fn write_image_list(
43    writer: &mut dyn Write,
44    assemblies: &[std::sync::Arc<Assembly>],
45) -> std::io::Result<()> {
46    for (idx, assembly) in assemblies.iter().enumerate() {
47        let start = assembly
48            .classes
49            .iter()
50            .map(|c| c.token & 0x00FF_FFFF)
51            .min()
52            .map(|t| t.saturating_sub(1))
53            .unwrap_or(0);
54
55        writeln!(writer, "// Image {}: {} - {}", idx, assembly.file, start)?;
56    }
57    writeln!(writer)
58}
59
60/// Resolves the dump directory based on an optional base path
61fn get_dump_dir(base_path: Option<&str>) -> Option<PathBuf> {
62    let root = if let Some(path) = base_path {
63        path.to_string()
64    } else {
65        Application::data_path().unwrap_or(".".to_string())
66    };
67
68    let dump_dir = Path::new(&root).join("dump");
69
70    if let Err(_e) = std::fs::create_dir_all(&dump_dir) {
71        #[cfg(dev_release)]
72        logger::error(&format!("Failed to create dump directory: {}", _e));
73        return None;
74    }
75
76    Some(dump_dir)
77}
78
79/// Dumps assemblies to separate files, or a single file if specified
80fn dump_assemblies_impl(base_path: Option<&str>, single_file_name: Option<&str>) -> Option<String> {
81    let dump_dir = get_dump_dir(base_path)?;
82
83    #[cfg(dev_release)]
84    logger::info(&format!("Dumping assemblies to {:?}...", dump_dir));
85
86    let assemblies = sorted_assemblies();
87
88    if let Some(file_name) = single_file_name {
89        // Dump all to one file
90        let path = dump_dir.join(file_name);
91        let file = match File::create(&path) {
92            Ok(f) => f,
93            Err(_e) => {
94                #[cfg(dev_release)]
95                logger::error(&format!("Failed to create dump file: {}", _e));
96                return None;
97            }
98        };
99        let mut writer = BufWriter::new(file);
100
101        // Image index block at the top
102        if let Err(_e) = write_image_list(&mut writer, &assemblies) {
103            #[cfg(dev_release)]
104            logger::error(&format!("Failed to write image list: {}", _e));
105        }
106
107        // Classes in sorted order
108        for assembly in &assemblies {
109            if let Err(_e) = write_assembly(&mut writer, assembly) {
110                #[cfg(dev_release)]
111                logger::error(&format!(
112                    "Failed to write assembly {}: {}",
113                    assembly.name, _e
114                ));
115            }
116        }
117
118        if let Err(_e) = writer.flush() {
119            #[cfg(dev_release)]
120            logger::error(&format!("Failed to flush writer: {}", _e));
121            return None;
122        }
123
124        #[cfg(dev_release)]
125        logger::info(&format!("Dumped all assemblies to {:?}", path));
126        Some(path.to_string_lossy().into_owned())
127    } else {
128        // Dump to separate files — each file still gets the full image list at the top
129        for assembly in &assemblies {
130            let path = dump_dir.join(format!("{}.cs", assembly.name));
131
132            let file = match File::create(&path) {
133                Ok(f) => f,
134                Err(_e) => {
135                    #[cfg(dev_release)]
136                    logger::error(&format!(
137                        "Failed to create dump file for {}: {}",
138                        assembly.name, _e
139                    ));
140                    continue;
141                }
142            };
143            let mut writer = BufWriter::new(file);
144
145            if let Err(_e) = write_image_list(&mut writer, &assemblies) {
146                #[cfg(dev_release)]
147                logger::error(&format!("Failed to write image list: {}", _e));
148            }
149
150            if let Err(_e) = write_assembly(&mut writer, assembly) {
151                #[cfg(dev_release)]
152                logger::error(&format!(
153                    "Failed to write assembly {}: {}",
154                    assembly.name, _e
155                ));
156            }
157
158            if writer.flush().is_ok() {
159                #[cfg(dev_release)]
160                logger::info(&format!("Successfully dumped assembly {}", assembly.name));
161            }
162        }
163
164        #[cfg(dev_release)]
165        logger::info("Dumped all assemblies");
166        Some(dump_dir.to_string_lossy().into_owned())
167    }
168}
169
170/// Dumps a single assembly into a `.cs` file under the default dump directory.
171///
172/// If `assembly_to_dump` is `None`, this falls back to [`dump_all`] for legacy
173/// behavior.
174pub fn dump_assembly(assembly_to_dump: Option<&str>) -> Option<()> {
175    // If no specific assembly, dump all to "dump.cs" (legacy behavior matches dump_all essentially)
176    if assembly_to_dump.is_none() {
177        return dump_all().map(|_| ());
178    }
179
180    let target_name = assembly_to_dump.unwrap();
181
182    let dump_dir = get_dump_dir(None)?;
183    let file_name = format!("{}.cs", target_name);
184    let path = dump_dir.join(file_name);
185
186    let file = match File::create(&path) {
187        Ok(f) => f,
188        Err(_e) => {
189            #[cfg(dev_release)]
190            logger::error(&format!("Failed to create dump file: {}", _e));
191            return None;
192        }
193    };
194    let mut writer = BufWriter::new(file);
195
196    #[cfg(dev_release)]
197    logger::info(&format!("Dumping assembly {}", target_name));
198
199    let assemblies = sorted_assemblies();
200
201    // Image index block at the top
202    if let Err(_e) = write_image_list(&mut writer, &assemblies) {
203        #[cfg(dev_release)]
204        logger::error(&format!("Failed to write image list: {}", _e));
205    }
206
207    for assembly in &assemblies {
208        if assembly.name.contains(target_name) {
209            if let Err(_e) = write_assembly(&mut writer, assembly) {
210                #[cfg(dev_release)]
211                logger::error(&format!("Failed to write assembly header: {}", _e));
212                return None;
213            }
214        }
215    }
216
217    if let Err(_e) = writer.flush() {
218        #[cfg(dev_release)]
219        logger::error(&format!("Failed to flush writer: {}", _e));
220        return None;
221    }
222
223    #[cfg(dev_release)]
224    logger::info(&format!("Dumped assembly to {:?}", path));
225    Some(())
226}
227
228/// Dumps all loaded assemblies into separate `.cs` files.
229///
230/// Returns the output directory path on success.
231pub fn dump() -> Option<String> {
232    dump_assemblies_impl(None, None)
233}
234
235/// Dumps all loaded assemblies into a single `dump.cs` file.
236///
237/// Returns the output file path on success.
238pub fn dump_all() -> Option<String> {
239    dump_assemblies_impl(None, Some("dump.cs"))
240}
241
242/// Dumps all loaded assemblies into separate `.cs` files under `base_path`.
243///
244/// Returns the output directory path on success.
245pub fn dump_to(base_path: &str) -> Option<String> {
246    dump_assemblies_impl(Some(base_path), None)
247}
248
249/// Dumps all loaded assemblies into a single `dump.cs` file under `base_path`.
250///
251/// Returns the output file path on success.
252pub fn dump_all_to(base_path: &str) -> Option<String> {
253    dump_assemblies_impl(Some(base_path), Some("dump.cs"))
254}