msg_tool\scripts\cat_system/
cstl.rs1use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::*;
6use anyhow::Result;
7use std::collections::BTreeMap;
8use std::io::{Read, Write};
9
10#[derive(Debug)]
11pub struct CstlScriptBuilder {}
13
14impl CstlScriptBuilder {
15 pub fn new() -> Self {
17 CstlScriptBuilder {}
18 }
19}
20
21impl ScriptBuilder for CstlScriptBuilder {
22 fn default_encoding(&self) -> Encoding {
23 Encoding::Utf8
24 }
25
26 fn build_script(
27 &self,
28 buf: Vec<u8>,
29 _filename: &str,
30 encoding: Encoding,
31 _archive_encoding: Encoding,
32 config: &ExtraConfig,
33 _archive: Option<&Box<dyn Script>>,
34 ) -> Result<Box<dyn Script>> {
35 Ok(Box::new(CstlScript::new(buf, encoding, config)?))
36 }
37
38 fn extensions(&self) -> &'static [&'static str] {
39 &["cstl"]
40 }
41
42 fn script_type(&self) -> &'static ScriptType {
43 &ScriptType::CatSystemCstl
44 }
45
46 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
47 if buf_len >= 4 && buf.starts_with(b"CSTL") {
48 return Some(15);
49 }
50 None
51 }
52
53 fn can_create_file(&self) -> bool {
54 true
55 }
56
57 fn create_file<'a>(
58 &'a self,
59 filename: &'a str,
60 writer: Box<dyn WriteSeek + 'a>,
61 encoding: Encoding,
62 file_encoding: Encoding,
63 config: &ExtraConfig,
64 ) -> Result<()> {
65 create_file(
66 filename,
67 writer,
68 encoding,
69 file_encoding,
70 config.custom_yaml,
71 )
72 }
73}
74
75pub fn create_file<T: Write>(
83 custom_filename: &str,
84 mut file: T,
85 encoding: Encoding,
86 output_encoding: Encoding,
87 yaml: bool,
88) -> Result<()> {
89 let input = crate::utils::files::read_file(custom_filename)?;
90 let s = decode_to_string(output_encoding, &input, true)?;
91 let data: BTreeMap<String, Vec<Message>> = if yaml {
92 serde_yaml_ng::from_str(&s)?
93 } else {
94 serde_json::from_str(&s)?
95 };
96 let count = data
97 .first_key_value()
98 .ok_or(anyhow::anyhow!("No data found in JSON"))?
99 .1
100 .len();
101 for (lang, mess) in &data {
102 if mess.len() != count {
103 return Err(anyhow::anyhow!(
104 "Language {lang} Message count mismatch: expected {}, got {}",
105 count,
106 mess.len()
107 ));
108 }
109 }
110 file.write_all(b"CSTL")?;
111 file.write_u32(0)?; let lang_count = data.len();
113 file.write_size(lang_count)?;
114 for lang in data.keys() {
115 let encoded = encode_string(encoding, lang, false)?;
116 file.write_size(encoded.len())?;
117 file.write_all(&encoded)?;
118 }
119 file.write_size(count)?;
120 for i in 0..count {
121 for mess in data.values() {
122 let m = &mess[i];
123 if let Some(name) = &m.name {
124 let encoded_name = encode_string(encoding, name, false)?;
125 file.write_size(encoded_name.len())?;
126 file.write_all(&encoded_name)?;
127 } else {
128 file.write_size(0)?;
129 }
130 let encoded_mes = encode_string(encoding, &m.message, false)?;
131 file.write_size(encoded_mes.len())?;
132 file.write_all(&encoded_mes)?;
133 }
134 }
135 Ok(())
136}
137
138trait CustomFn {
139 fn read_size(&mut self) -> Result<usize>;
140}
141
142impl<T: Read> CustomFn for T {
143 fn read_size(&mut self) -> Result<usize> {
144 let mut size = 0;
145 loop {
146 let len = self.read_u8()?;
147 size += len as usize;
148 if len != 0xFF {
149 break;
150 }
151 }
152 Ok(size)
153 }
154}
155
156trait CustomWriteFn {
157 fn write_size(&mut self, size: usize) -> Result<()>;
158}
159
160impl<T: Write> CustomWriteFn for T {
161 fn write_size(&mut self, mut size: usize) -> Result<()> {
162 loop {
163 let len = if size > 0xFF { 0xFF } else { size as u8 };
164 self.write_u8(len)?;
165 size -= len as usize;
166 if len != 0xFF {
167 break;
168 }
169 }
170 Ok(())
171 }
172}
173
174#[derive(Debug)]
175pub struct CstlScript {
177 langs: Vec<String>,
178 data: Vec<Vec<Message>>,
179 lang_index: Option<usize>,
180 custom_yaml: bool,
181}
182
183impl CstlScript {
184 pub fn new(buf: Vec<u8>, encoding: Encoding, config: &ExtraConfig) -> Result<Self> {
190 let mut langs = Vec::new();
191 let mut data = Vec::new();
192 let mut reader = MemReader::new(buf);
193 let mut magic = [0; 4];
194 reader.read_exact(&mut magic)?;
195 if &magic != b"CSTL" {
196 return Err(anyhow::anyhow!("Invalid CSTL magic number"));
197 }
198 let unk = reader.read_u32()?;
199 if unk != 0 {
200 return Err(anyhow::anyhow!("Unknown CSTL unk value: {}", unk));
201 }
202 let lang_count = reader.read_size()?;
203 for _ in 0..lang_count {
204 let len = reader.read_size()?;
205 let s = reader.read_fstring(len, encoding, false)?;
206 langs.push(s);
207 data.push(Vec::new());
208 }
209 let count = reader.read_size()?;
210 let mut i = 0;
211 loop {
212 let name_len = reader.read_size()?;
213 let name = if name_len > 0 {
214 Some(reader.read_fstring(name_len, encoding, false)?)
215 } else {
216 None
217 };
218 let mes_len = reader.read_size()?;
219 let message = reader.read_fstring(mes_len, encoding, false)?;
220 data[i % lang_count].push(Message { name, message });
221 i += 1;
222 if reader.is_eof() {
223 break;
224 }
225 }
226 if i != count * lang_count {
227 return Err(anyhow::anyhow!(
228 "CSTL data count mismatch: expected {}, got {}",
229 i,
230 count * langs.len()
231 ));
232 }
233 for (i, lang) in langs.iter().enumerate() {
234 if data[i].len() != count {
235 return Err(anyhow::anyhow!(
236 "CSTL language '{}' data count mismatch: expected {}, got {}",
237 lang,
238 count,
239 data[i].len()
240 ));
241 }
242 }
243 let lang_index = config
244 .cat_system_cstl_lang
245 .as_ref()
246 .and_then(|lang| langs.iter().position(|l| l == lang));
247 if config.cat_system_cstl_lang.is_some() && lang_index.is_none() {
248 eprintln!(
249 "Warning: specified language '{}' not found in CSTL script",
250 config.cat_system_cstl_lang.as_ref().unwrap()
251 );
252 crate::COUNTER.inc_warning();
253 }
254 Ok(CstlScript {
255 langs,
256 data,
257 lang_index,
258 custom_yaml: config.custom_yaml,
259 })
260 }
261}
262
263impl Script for CstlScript {
264 fn default_output_script_type(&self) -> OutputScriptType {
265 OutputScriptType::Json
266 }
267
268 fn default_format_type(&self) -> FormatOptions {
269 FormatOptions::None
270 }
271
272 fn is_output_supported(&self, _: OutputScriptType) -> bool {
273 true
274 }
275
276 fn custom_output_extension<'a>(&'a self) -> &'a str {
277 if self.custom_yaml { "yaml" } else { "json" }
278 }
279
280 fn extract_messages(&self) -> Result<Vec<Message>> {
281 if self.langs.is_empty() || self.data.is_empty() {
282 return Err(anyhow::anyhow!("CSTL script has no languages or data"));
283 }
284 Ok(self.data[self.lang_index.unwrap_or(0)]
285 .iter()
286 .map(|m| Message {
287 name: m.name.clone(),
288 message: m.message.replace("\\n", "\n"),
289 })
290 .collect())
291 }
292
293 fn import_messages<'a>(
294 &'a self,
295 messages: Vec<Message>,
296 mut file: Box<dyn WriteSeek + 'a>,
297 _filename: &str,
298 encoding: Encoding,
299 replacement: Option<&'a ReplacementTable>,
300 ) -> Result<()> {
301 let mut data = self.data.clone();
302 let index = self.lang_index.unwrap_or(0);
303 if data[index].len() != messages.len() {
304 return Err(anyhow::anyhow!(
305 "CSTL script language '{}' message count mismatch: expected {}, got {}",
306 self.langs[index],
307 data[index].len(),
308 messages.len()
309 ));
310 }
311 for (i, m) in data[index].iter_mut().enumerate() {
312 if let Some(n) = &mut m.name {
313 let mut name = match &messages[i].name {
314 Some(name) => name.clone(),
315 None => return Err(anyhow::anyhow!("Message {i} name is missing.")),
316 };
317 if let Some(replacement) = replacement {
318 for (k, v) in &replacement.map {
319 name = name.replace(k, v);
320 }
321 }
322 *n = name;
323 }
324 let mut mes = messages[i].message.clone();
325 if let Some(replacement) = replacement {
326 for (k, v) in &replacement.map {
327 mes = mes.replace(k, v);
328 }
329 }
330 m.message = mes.replace("\n", "\\n");
331 }
332 file.write_all(b"CSTL")?;
333 file.write_u32(0)?; let lang_count = self.langs.len();
335 file.write_size(lang_count)?;
336 for lang in &self.langs {
337 let encoded = encode_string(encoding, &lang, false)?;
338 file.write_size(encoded.len())?;
339 file.write_all(&encoded)?;
340 }
341 let count = data[index].len();
342 file.write_size(count)?;
343 for i in 0..count {
344 for j in 0..lang_count {
345 let m = &data[j][i];
346 if let Some(name) = &m.name {
347 let encoded_name = encode_string(encoding, name, false)?;
348 file.write_size(encoded_name.len())?;
349 file.write_all(&encoded_name)?;
350 } else {
351 file.write_size(0)?;
352 }
353 let encoded_mes = encode_string(encoding, &m.message, false)?;
354 file.write_size(encoded_mes.len())?;
355 file.write_all(&encoded_mes)?;
356 }
357 }
358 Ok(())
359 }
360
361 fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> {
362 let mut d = BTreeMap::new();
363 for (lang, data) in self.langs.iter().zip(&self.data) {
364 d.insert(lang, data);
365 }
366 let s = if self.custom_yaml {
367 serde_yaml_ng::to_string(&d)
368 .map_err(|e| anyhow::anyhow!("Failed to serialize to YAML: {}", e))?
369 } else {
370 serde_json::to_string(&d)
371 .map_err(|e| anyhow::anyhow!("Failed to serialize to JSON: {}", e))?
372 };
373 let s = encode_string(encoding, &s, false)?;
374 let mut file = std::fs::File::create(filename)?;
375 file.write_all(&s)?;
376 Ok(())
377 }
378
379 fn custom_import<'a>(
380 &'a self,
381 custom_filename: &'a str,
382 file: Box<dyn WriteSeek + 'a>,
383 encoding: Encoding,
384 output_encoding: Encoding,
385 ) -> Result<()> {
386 create_file(
387 custom_filename,
388 file,
389 encoding,
390 output_encoding,
391 self.custom_yaml,
392 )
393 }
394}