Skip to main content

ctr_rsf/
lib.rs

1pub mod rng;
2
3pub const DUMMY_RSF: &str = include_str!("../dummy.rsf");
4
5use serde::{Deserialize, Serialize};
6use std::{fs, path::Path};
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum RsfError {
11    #[error("I/O error: {0}")]
12    Io(#[from] std::io::Error),
13    #[error("YAML parse/serialize error: {0}")]
14    Yaml(#[from] serde_yaml::Error),
15}
16
17/// Top-level RSF structure
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Rsf {
20    #[serde(rename = "BasicInfo")]
21    pub basic_info: BasicInfo,
22
23    #[serde(rename = "RomFs")]
24    pub rom_fs: Option<RomFs>,
25
26    #[serde(rename = "TitleInfo")]
27    pub title_info: TitleInfo,
28
29    #[serde(rename = "Option")]
30    pub option: OptionSection,
31
32    #[serde(rename = "AccessControlInfo")]
33    pub access_control_info: AccessControlInfo,
34
35    #[serde(rename = "SystemControlInfo")]
36    pub system_control_info: SystemControlInfo,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct BasicInfo {
41    #[serde(rename = "Title")]
42    pub title: String,
43    #[serde(rename = "ProductCode")]
44    pub product_code: String,
45    #[serde(rename = "Logo")]
46    pub logo: String,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct RomFs {
51    #[serde(rename = "RootPath")]
52    pub root_path: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TitleInfo {
57    #[serde(rename = "Category")]
58    pub category: String,
59    #[serde(rename = "UniqueId")]
60    pub unique_id: u32,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct OptionSection {
65    #[serde(rename = "UseOnSD")]
66    pub use_on_sd: bool,
67    #[serde(rename = "FreeProductCode")]
68    pub free_product_code: bool,
69    #[serde(rename = "MediaFootPadding")]
70    pub media_foot_padding: bool,
71    #[serde(rename = "EnableCrypt")]
72    pub enable_crypt: bool,
73    #[serde(rename = "EnableCompress")]
74    pub enable_compress: bool,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct AccessControlInfo {
79    #[serde(rename = "CoreVersion")]
80    pub core_version: u32,
81
82    #[serde(rename = "DescVersion")]
83    pub desc_version: u32,
84
85    #[serde(rename = "ReleaseKernelMajor")]
86    pub release_kernel_major: String,
87
88    #[serde(rename = "ReleaseKernelMinor")]
89    pub release_kernel_minor: String,
90
91    #[serde(rename = "UseExtSaveData")]
92    pub use_ext_save_data: bool,
93
94    #[serde(rename = "FileSystemAccess")]
95    pub file_system_access: Option<Vec<String>>,
96
97    #[serde(rename = "MemoryType")]
98    pub memory_type: String,
99    #[serde(rename = "SystemMode")]
100    pub system_mode: String,
101    #[serde(rename = "IdealProcessor")]
102    pub ideal_processor: u8,
103    #[serde(rename = "AffinityMask")]
104    pub affinity_mask: u8,
105    #[serde(rename = "Priority")]
106    pub priority: u8,
107    #[serde(rename = "MaxCpu")]
108    pub max_cpu: u8,
109    #[serde(rename = "HandleTableSize")]
110    pub handle_table_size: u32,
111    #[serde(rename = "DisableDebug")]
112    pub disable_debug: bool,
113    #[serde(rename = "EnableForceDebug")]
114    pub enable_force_debug: bool,
115    #[serde(rename = "CanWriteSharedPage")]
116    pub can_write_shared_page: bool,
117    #[serde(rename = "CanUsePrivilegedPriority")]
118    pub can_use_privileged_priority: bool,
119    #[serde(rename = "CanUseNonAlphabetAndNumber")]
120    pub can_use_non_alphabet_and_number: bool,
121    #[serde(rename = "PermitMainFunctionArgument")]
122    pub permit_main_function_argument: bool,
123    #[serde(rename = "CanShareDeviceMemory")]
124    pub can_share_device_memory: bool,
125    #[serde(rename = "RunnableOnSleep")]
126    pub runnable_on_sleep: bool,
127    #[serde(rename = "SpecialMemoryArrange")]
128    pub special_memory_arrange: bool,
129
130    #[serde(rename = "SystemModeExt")]
131    pub system_mode_ext: String,
132    #[serde(rename = "CpuSpeed")]
133    pub cpu_speed: String,
134    #[serde(rename = "EnableL2Cache")]
135    pub enable_l2_cache: bool,
136    #[serde(rename = "CanAccessCore2")]
137    pub can_access_core2: bool,
138
139    #[serde(rename = "IORegisterMapping")]
140    pub io_register_mapping: Option<Vec<String>>,
141    #[serde(rename = "MemoryMapping")]
142    pub memory_mapping: Option<Vec<String>>,
143
144    #[serde(rename = "SystemCallAccess")]
145    pub system_call_access: Option<std::collections::HashMap<String, u32>>,
146
147    #[serde(rename = "ServiceAccessControl")]
148    pub service_access_control: Option<Vec<String>>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct SystemControlInfo {
153    #[serde(rename = "SaveDataSize")]
154    pub save_data_size: String,
155    #[serde(rename = "RemasterVersion")]
156    pub remaster_version: u32,
157    #[serde(rename = "StackSize")]
158    pub stack_size: String,
159
160    #[serde(rename = "Dependency")]
161    pub dependency: Option<std::collections::HashMap<String, String>>,
162}
163
164/// Load an RSF file from disk.
165pub fn load_rsf<P: AsRef<Path>>(path: P) -> Result<Rsf, RsfError> {
166    let text = fs::read_to_string(path)?;
167    let rsf: Rsf = serde_yaml::from_str(&text)?;
168    Ok(rsf)
169}
170
171/// Save an RSF structure back to disk.
172pub fn save_rsf<P: AsRef<Path>>(path: P, rsf: &Rsf) -> Result<(), RsfError> {
173    let text = serde_yaml::to_string(rsf)?;
174    fs::write(path, text)?;
175    Ok(())
176}
177
178/// Returns true only if the product code contains A–Z, a–z, or 0–9.
179pub fn is_valid_product_code(code: &str) -> bool {
180    code.chars().all(|c| c.is_ascii_alphanumeric())
181}
182
183/// Removes all characters that are NOT A–Z, a–z, or 0–9.
184pub fn sanitize_product_code(code: &str) -> String {
185    code.chars()
186        .filter(|c| c.is_ascii_alphanumeric())
187        .collect()
188}
189
190impl BasicInfo {
191    pub fn set_product_code(&mut self, code: &str) -> Result<(), &'static str> {
192        if is_valid_product_code(code) {
193            self.product_code = code.to_string();
194            Ok(())
195        } else {
196            Err("ProductCode contains invalid characters")
197        }
198    }
199}
200
201use serde_yaml;
202
203/// Load the embedded dummy.rsf into an Rsf struct.
204pub fn load_embedded_rsf() -> Result<Rsf, serde_yaml::Error> {
205    serde_yaml::from_str(DUMMY_RSF)
206}
207
208/// Load RSF but treat unknown YAML tags (like @VAR@) as plain strings.
209pub fn load_rsf_lenient(raw: &str) -> Result<serde_yaml::Value, serde_yaml::Error> {
210    let deserializer = serde_yaml::Deserializer::from_str(raw);
211    let value = serde_yaml::Value::deserialize(deserializer)?;
212    Ok(value)
213}
214
215pub fn load_rsf_safe(raw: &str) -> Result<Rsf, serde_yaml::Error> {
216    let sanitized = sanitize_rsf(raw);
217    serde_yaml::from_str(&sanitized)
218}
219
220
221pub fn sanitize_rsf(raw: &str) -> String {
222    raw.lines()
223        .map(|line| {
224            if let Some((key, value)) = line.split_once(':') {
225                let trimmed = value.trim();
226
227                // Already quoted? Leave it alone.
228                if trimmed.starts_with('"') || trimmed.starts_with('\'') {
229                    return line.to_string();
230                }
231
232                // If the value contains @...@, quote it.
233                if trimmed.contains('@') {
234                    return format!("{key}: \"{trimmed}\"");
235                }
236
237                line.to_string()
238            } else {
239                line.to_string()
240            }
241        })
242        .collect::<Vec<_>>()
243        .join("\n")
244}
245
246