Skip to main content

siglus_cfx_decompiler/
names.rs

1use std::collections::BTreeSet;
2use std::path::Path;
3
4use crate::cfx::ShaderBlob;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum StringEncoding {
8    Ascii,
9    Utf16Le,
10}
11
12impl StringEncoding {
13    pub fn as_str(self) -> &'static str {
14        match self {
15            Self::Ascii => "ascii",
16            Self::Utf16Le => "utf16le",
17        }
18    }
19}
20
21#[derive(Debug, Clone)]
22pub struct ExtractedString {
23    pub offset: usize,
24    pub encoding: StringEncoding,
25    pub text: String,
26}
27
28pub fn extract_strings(data: &[u8]) -> Vec<ExtractedString> {
29    let mut out = Vec::new();
30    out.extend(extract_ascii_strings(data, 3));
31    out.extend(extract_utf16le_strings(data, 3));
32    out.sort_by_key(|s| {
33        (
34            s.offset,
35            match s.encoding {
36                StringEncoding::Ascii => 0u8,
37                StringEncoding::Utf16Le => 1u8,
38            },
39        )
40    });
41    out.dedup_by(|a, b| a.offset == b.offset && a.encoding == b.encoding && a.text == b.text);
42    out
43}
44
45fn extract_ascii_strings(data: &[u8], min_len: usize) -> Vec<ExtractedString> {
46    let mut out = Vec::new();
47    let mut i = 0usize;
48    while i < data.len() {
49        while i < data.len() && !is_printable_ascii(data[i]) {
50            i += 1;
51        }
52        let start = i;
53        while i < data.len() && is_printable_ascii(data[i]) {
54            i += 1;
55        }
56        if i.saturating_sub(start) >= min_len {
57            let text = String::from_utf8_lossy(&data[start..i]).to_string();
58            out.push(ExtractedString {
59                offset: start,
60                encoding: StringEncoding::Ascii,
61                text,
62            });
63        }
64    }
65    out
66}
67
68fn extract_utf16le_strings(data: &[u8], min_chars: usize) -> Vec<ExtractedString> {
69    let mut out = Vec::new();
70    let mut i = 0usize;
71    while i + 1 < data.len() {
72        while i + 1 < data.len() && !is_printable_utf16le_at(data, i) {
73            i += 2;
74        }
75        let start = i;
76        let mut chars = Vec::new();
77        while i + 1 < data.len() && is_printable_utf16le_at(data, i) {
78            chars.push(u16::from_le_bytes([data[i], data[i + 1]]));
79            i += 2;
80        }
81        if chars.len() >= min_chars
82            && let Ok(text) = String::from_utf16(&chars)
83        {
84            out.push(ExtractedString {
85                offset: start,
86                encoding: StringEncoding::Utf16Le,
87                text,
88            });
89        }
90        if i == start {
91            i += 2;
92        }
93    }
94    out
95}
96
97fn is_printable_ascii(b: u8) -> bool {
98    matches!(b, 0x20..=0x7e)
99}
100
101fn is_printable_utf16le_at(data: &[u8], off: usize) -> bool {
102    if off + 1 >= data.len() {
103        return false;
104    }
105    let c = u16::from_le_bytes([data[off], data[off + 1]]);
106    matches!(c, 0x20..=0x7e)
107}
108
109pub fn is_hlsl_identifier(s: &str) -> bool {
110    let mut chars = s.chars();
111    let Some(first) = chars.next() else {
112        return false;
113    };
114    if !(first == '_' || first.is_ascii_alphabetic()) {
115        return false;
116    }
117    chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
118}
119
120pub fn looks_like_technique_name(s: &str) -> bool {
121    s.starts_with("tec_") || s.starts_with("tech_") || s.starts_with("technique")
122}
123
124pub fn looks_like_shader_function_name(s: &str) -> bool {
125    if !is_hlsl_identifier(s) {
126        return false;
127    }
128    let lower = s.to_ascii_lowercase();
129    lower.starts_with("vs_")
130        || lower.starts_with("ps_")
131        || lower.starts_with("v_")
132        || lower.starts_with("p_")
133        || lower.starts_with("vertex")
134        || lower.starts_with("pixel")
135}
136
137pub fn looks_like_interface_or_struct_name(s: &str) -> bool {
138    if !is_hlsl_identifier(s) {
139        return false;
140    }
141    let lower = s.to_ascii_lowercase();
142    lower.contains("input")
143        || lower.contains("output")
144        || lower.ends_with("_in")
145        || lower.ends_with("_out")
146        || lower.ends_with("interface")
147        || lower.ends_with("struct")
148}
149
150pub fn format_original_name_report(input: &Path, data: &[u8], shaders: &[ShaderBlob]) -> String {
151    let strings = extract_strings(data);
152    let mut identifiers = BTreeSet::new();
153    let mut techniques = BTreeSet::new();
154    let mut shader_function_candidates = BTreeSet::new();
155    let mut interface_candidates = BTreeSet::new();
156
157    for s in &strings {
158        if is_hlsl_identifier(&s.text) {
159            identifiers.insert(s.text.clone());
160        }
161        if looks_like_technique_name(&s.text) {
162            techniques.insert(s.text.clone());
163        }
164        if looks_like_shader_function_name(&s.text) {
165            shader_function_candidates.insert(s.text.clone());
166        }
167        if looks_like_interface_or_struct_name(&s.text) {
168            interface_candidates.insert(s.text.clone());
169        }
170    }
171
172    let mut out = String::new();
173    out.push_str(&format!("input: {}\n", input.display()));
174    out.push('\n');
175    out.push_str("name_recovery_policy:\n");
176    out.push_str("  exact_shader_entry_function_name: only used if explicitly stored in source/debug/effect metadata; SM2 bytecode CTAB does not carry it.\n");
177    out.push_str("  exact_interface_type_name: only used if explicitly stored in source/debug/effect metadata; SM2 bytecode declarations carry semantics, not original HLSL struct/interface type names.\n");
178    out.push_str("  uniforms_samplers_and_struct_members: recovered from CTAB when present.\n");
179    out.push_str("  technique_pass_names: recovered only after effect-container parsing; raw shader scanning alone cannot map them to a shader safely.\n");
180    out.push('\n');
181
182    out.push_str("shader_ctab_names:\n");
183    for shader in shaders {
184        out.push_str(&format!(
185            "  {} offset=0x{:08x} profile={}\n",
186            shader.file_prefix(),
187            shader.offset,
188            shader.profile()
189        ));
190        if let Some(ctab) = &shader.ctab {
191            if let Some(creator) = &ctab.creator {
192                out.push_str(&format!("    creator: {}\n", creator));
193            }
194            if let Some(target) = &ctab.target {
195                out.push_str(&format!("    target: {}\n", target));
196            }
197            for c in &ctab.constants {
198                out.push_str(&format!(
199                    "    {} {} {} count={}\n",
200                    c.register_name(),
201                    c.hlsl_decl_type(),
202                    c.name,
203                    c.register_count
204                ));
205                if let Some(t) = &c.type_info {
206                    for m in &t.members {
207                        out.push_str(&format!(
208                            "      member {} {}\n",
209                            m.type_info.hlsl_type_name(),
210                            m.name
211                        ));
212                    }
213                }
214            }
215        } else {
216            out.push_str("    no CTAB\n");
217        }
218    }
219
220    out.push_str("\ntechnique_name_candidates_from_container_strings:\n");
221    for name in techniques.iter().take(2000) {
222        out.push_str(&format!("  {}\n", name));
223    }
224    if techniques.len() > 2000 {
225        out.push_str(&format!("  ... {} more\n", techniques.len() - 2000));
226    }
227
228    out.push_str("\nshader_function_name_candidates_from_container_strings:\n");
229    for name in shader_function_candidates.iter().take(2000) {
230        out.push_str(&format!("  {}\n", name));
231    }
232    if shader_function_candidates.len() > 2000 {
233        out.push_str(&format!(
234            "  ... {} more\n",
235            shader_function_candidates.len() - 2000
236        ));
237    }
238
239    out.push_str("\ninterface_or_struct_name_candidates_from_container_strings:\n");
240    for name in interface_candidates.iter().take(2000) {
241        out.push_str(&format!("  {}\n", name));
242    }
243    if interface_candidates.len() > 2000 {
244        out.push_str(&format!(
245            "  ... {} more\n",
246            interface_candidates.len() - 2000
247        ));
248    }
249
250    out.push_str("\nall_identifier_strings_from_container:\n");
251    for name in identifiers.iter().take(5000) {
252        out.push_str(&format!("  {}\n", name));
253    }
254    if identifiers.len() > 5000 {
255        out.push_str(&format!("  ... {} more\n", identifiers.len() - 5000));
256    }
257
258    out.push_str("\nall_strings_with_offsets:\n");
259    for s in strings.iter().take(10000) {
260        out.push_str(&format!(
261            "  0x{:08x} {} {}\n",
262            s.offset,
263            s.encoding.as_str(),
264            s.text
265        ));
266    }
267    if strings.len() > 10000 {
268        out.push_str(&format!("  ... {} more\n", strings.len() - 10000));
269    }
270
271    out
272}