msg_tool\scripts\yaneurao\itufuru/
archive.rs1use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::encode_string;
6use crate::utils::struct_pack::*;
7use crate::utils::xored_stream::XoredStream as Crypto;
8use anyhow::Result;
9use msg_tool_macro::*;
10use std::collections::HashMap;
11use std::io::{Read, Seek, SeekFrom, Write};
12use std::sync::{Arc, Mutex};
13
14#[derive(Debug)]
15pub struct ItufuruArchiveBuilder {}
17
18impl ItufuruArchiveBuilder {
19 pub const fn new() -> Self {
21 ItufuruArchiveBuilder {}
22 }
23}
24
25impl ScriptBuilder for ItufuruArchiveBuilder {
26 fn default_encoding(&self) -> Encoding {
27 Encoding::Cp932
28 }
29
30 fn default_archive_encoding(&self) -> Option<Encoding> {
31 Some(Encoding::Cp932)
32 }
33
34 fn build_script(
35 &self,
36 data: Vec<u8>,
37 _filename: &str,
38 _encoding: Encoding,
39 archive_encoding: Encoding,
40 config: &ExtraConfig,
41 _archive: Option<&Box<dyn Script>>,
42 ) -> Result<Box<dyn Script + Send + Sync>> {
43 Ok(Box::new(ItufuruArchive::new(
44 MemReader::new(data),
45 archive_encoding,
46 config,
47 )?))
48 }
49
50 fn build_script_from_file(
51 &self,
52 filename: &str,
53 _encoding: Encoding,
54 archive_encoding: Encoding,
55 config: &ExtraConfig,
56 _archive: Option<&Box<dyn Script>>,
57 ) -> Result<Box<dyn Script + Send + Sync>> {
58 if filename == "-" {
59 let data = crate::utils::files::read_file(filename)?;
60 Ok(Box::new(ItufuruArchive::new(
61 MemReader::new(data),
62 archive_encoding,
63 config,
64 )?))
65 } else {
66 let f = std::fs::File::open(filename)?;
67 let reader = std::io::BufReader::new(f);
68 Ok(Box::new(ItufuruArchive::new(
69 reader,
70 archive_encoding,
71 config,
72 )?))
73 }
74 }
75
76 fn build_script_from_reader<'a>(
77 &self,
78 reader: Box<dyn ReadSeek + Send + Sync + 'a>,
79 _filename: &str,
80 _encoding: Encoding,
81 archive_encoding: Encoding,
82 config: &ExtraConfig,
83 _archive: Option<&Box<dyn Script>>,
84 ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
85 Ok(Box::new(ItufuruArchive::new(
86 reader,
87 archive_encoding,
88 config,
89 )?))
90 }
91
92 fn extensions(&self) -> &'static [&'static str] {
93 &["scd"]
94 }
95
96 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
97 if buf_len >= 4 && buf.starts_with(b"SCR\0") {
98 Some(1)
99 } else {
100 None
101 }
102 }
103
104 fn script_type(&self) -> &'static ScriptType {
105 &ScriptType::YaneuraoItufuruArc
106 }
107
108 fn is_archive(&self) -> bool {
109 true
110 }
111
112 fn create_archive(
113 &self,
114 filename: &str,
115 files: &[&str],
116 encoding: Encoding,
117 config: &ExtraConfig,
118 ) -> Result<Box<dyn Archive>> {
119 let f = std::fs::File::create(filename)?;
120 let writer = std::io::BufWriter::new(f);
121 let archive = ItufuruArchiveWriter::new(writer, files, encoding, config)?;
122 Ok(Box::new(archive))
123 }
124}
125
126#[derive(Debug, StructPack, StructUnpack)]
127struct ItufuruFileHeader {
128 #[fstring = 12]
129 file_name: String,
130 offset: u32,
131}
132
133#[derive(Debug, StructPack)]
134struct CustomHeader {
135 #[fstring = 12]
136 file_name: String,
137 offset: u32,
138 #[skip_pack]
139 size: u32,
140}
141
142struct Entry {
143 name: String,
144 data: MemReader,
145}
146
147impl Read for Entry {
148 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
149 self.data.read(buf)
150 }
151}
152
153impl ArchiveContent for Entry {
154 fn name(&self) -> &str {
155 &self.name
156 }
157
158 fn size(&self) -> Option<u64> {
159 Some(self.data.data.len() as u64)
160 }
161
162 fn script_type(&self) -> Option<&ScriptType> {
163 Some(&ScriptType::YaneuraoItufuru)
164 }
165
166 fn data(&mut self) -> Result<Vec<u8>> {
167 Ok(self.data.data.clone())
168 }
169
170 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
171 Ok(Box::new(&mut self.data))
172 }
173}
174
175#[derive(Debug)]
176pub struct ItufuruArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
178 reader: Arc<Mutex<Crypto<T>>>,
179 first_file_offset: u32,
180 files: Vec<CustomHeader>,
181 _mark: std::marker::PhantomData<&'b ()>,
182}
183
184impl<'b, T: Read + Seek + std::fmt::Debug + 'b> ItufuruArchive<'b, T> {
185 pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
191 let mut header = [0u8; 4];
192 reader.read_exact(&mut header)?;
193 if &header != b"SCR\0" {
194 return Err(anyhow::anyhow!("Invalid Itufuru archive header"));
195 }
196 let file_count = reader.read_u32()?;
197 let first_file_offset = reader.read_u32()?;
198 reader.read_u32()?; let mut reader = Crypto::new(reader, 0xA5);
200 let mut tfiles = Vec::with_capacity(file_count as usize);
201 for _ in 0..file_count {
202 let file = ItufuruFileHeader::unpack(&mut reader, false, archive_encoding, &None)?;
203 tfiles.push(file);
204 }
205 let mut files = Vec::with_capacity(tfiles.len());
206 if !tfiles.is_empty() {
207 for i in 0..tfiles.len() - 1 {
208 let file = CustomHeader {
209 file_name: tfiles[i].file_name.clone(),
210 offset: tfiles[i].offset,
211 size: tfiles[i + 1].offset - tfiles[i].offset,
212 };
213 files.push(file);
214 }
215 let last_file = &tfiles[tfiles.len() - 1];
216 let file = CustomHeader {
217 file_name: last_file.file_name.clone(),
218 offset: last_file.offset,
219 size: reader.seek(SeekFrom::End(0))? as u32 - last_file.offset - first_file_offset,
220 };
221 files.push(file);
222 }
223 Ok(ItufuruArchive {
224 reader: Arc::new(Mutex::new(reader)),
225 first_file_offset,
226 files,
227 _mark: std::marker::PhantomData,
228 })
229 }
230}
231
232impl<'b, T: Read + Seek + std::fmt::Debug + 'b> Script for ItufuruArchive<'b, T> {
233 fn default_output_script_type(&self) -> OutputScriptType {
234 OutputScriptType::Json
235 }
236
237 fn default_format_type(&self) -> FormatOptions {
238 FormatOptions::None
239 }
240
241 fn is_archive(&self) -> bool {
242 true
243 }
244
245 fn iter_archive_filename<'a>(
246 &'a self,
247 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
248 Ok(Box::new(
249 self.files.iter().map(|s| Ok(s.file_name.to_owned())),
250 ))
251 }
252
253 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
254 Ok(Box::new(self.files.iter().map(|s| Ok(s.offset as u64))))
255 }
256
257 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
258 if index >= self.files.len() {
259 return Err(anyhow::anyhow!(
260 "Index out of bounds: {} (max: {})",
261 index,
262 self.files.len()
263 ));
264 }
265 let entry = &self.files[index];
266 let file_offset = entry.offset as u64;
267 match self.reader.cpeek_exact_at_vec(
268 file_offset + self.first_file_offset as u64,
269 entry.size as usize,
270 ) {
271 Ok(data) => {
272 let name = entry.file_name.clone();
273 Ok(Box::new(Entry {
274 name,
275 data: MemReader::new(data),
276 }))
277 }
278 Err(e) => Err(anyhow::anyhow!(
279 "Failed to read file {}: {}",
280 entry.file_name,
281 e
282 )),
283 }
284 }
285}
286
287pub struct ItufuruArchiveWriter<T: Write + Seek> {
289 writer: T,
290 headers: HashMap<String, CustomHeader>,
291 first_file_offset: u32,
292 encoding: Encoding,
293}
294
295impl<T: Write + Seek> ItufuruArchiveWriter<T> {
296 pub fn new(
303 mut writer: T,
304 files: &[&str],
305 encoding: Encoding,
306 _config: &ExtraConfig,
307 ) -> Result<Self> {
308 writer.write_all(b"SCR\0")?;
309 let file_count = files.len() as u32;
310 writer.write_u32(file_count)?;
311 let first_file_offset = 0x10 + file_count * 16; writer.write_u32(first_file_offset)?;
313 writer.write_u32(0)?; let mut headers = HashMap::new();
315 for file in files {
316 headers.insert(
317 file.to_string(),
318 CustomHeader {
319 file_name: file.to_string(),
320 offset: 0,
321 size: 0,
322 },
323 );
324 }
325 let mut crypto = Crypto::new(&mut writer, 0xA5);
326 for (_, header) in headers.iter() {
327 header.pack(&mut crypto, false, encoding, &None)?;
328 }
329 Ok(ItufuruArchiveWriter {
330 writer,
331 headers,
332 first_file_offset,
333 encoding,
334 })
335 }
336}
337
338impl<T: Write + Seek> Archive for ItufuruArchiveWriter<T> {
339 fn new_file<'a>(
340 &'a mut self,
341 name: &str,
342 _size: Option<u64>,
343 ) -> Result<Box<dyn WriteSeek + 'a>> {
344 let entry = self
345 .headers
346 .get_mut(name)
347 .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
348 if entry.size != 0 {
349 return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
350 }
351 entry.offset = self.writer.stream_position()? as u32 - self.first_file_offset;
352 Ok(Box::new(ItufuruArchiveWriterEntry::new(
353 &mut self.writer,
354 entry,
355 self.first_file_offset,
356 )))
357 }
358 fn write_header(&mut self) -> Result<()> {
359 let mut crypto = Crypto::new(&mut self.writer, 0xA5);
360 let mut entries = self.headers.values().collect::<Vec<_>>();
361 entries.sort_by_key(|h| h.offset);
362 crypto.seek(SeekFrom::Start(16))?;
363 for entry in entries.iter() {
364 entry.pack(&mut crypto, false, self.encoding, &None)?;
365 }
366 Ok(())
367 }
368}
369
370pub struct ItufuruArchiveWriterEntry<'a, T: Write + Seek> {
372 writer: Crypto<&'a mut T>,
373 header: &'a mut CustomHeader,
374 first_file_offset: u32,
375 pos: usize,
376}
377
378impl<'a, T: Write + Seek> ItufuruArchiveWriterEntry<'a, T> {
379 fn new(writer: &'a mut T, header: &'a mut CustomHeader, first_file_offset: u32) -> Self {
380 let writer = Crypto::new(writer, 0xA5);
381 ItufuruArchiveWriterEntry {
382 writer,
383 header,
384 first_file_offset,
385 pos: 0,
386 }
387 }
388}
389
390impl<'a, T: Write + Seek> Write for ItufuruArchiveWriterEntry<'a, T> {
391 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
392 self.writer.seek(SeekFrom::Start(
393 self.header.offset as u64 + self.first_file_offset as u64 + self.pos as u64,
394 ))?;
395 let written = self.writer.write(buf)?;
396 self.pos += written;
397 self.header.size = self.header.size.max(self.pos as u32);
398 Ok(written)
399 }
400
401 fn flush(&mut self) -> std::io::Result<()> {
402 self.writer.flush()
403 }
404}
405
406impl<'a, T: Write + Seek> Seek for ItufuruArchiveWriterEntry<'a, T> {
407 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
408 let new_pos = match pos {
409 SeekFrom::Start(offset) => offset as usize,
410 SeekFrom::End(offset) => {
411 if offset < 0 {
412 if (-offset) as usize > self.header.size as usize {
413 return Err(std::io::Error::new(
414 std::io::ErrorKind::InvalidInput,
415 "Seek from end exceeds file length",
416 ));
417 }
418 self.header.size as usize - (-offset) as usize
419 } else {
420 self.header.size as usize + offset as usize
421 }
422 }
423 SeekFrom::Current(offset) => {
424 if offset < 0 {
425 if (-offset) as usize > self.pos {
426 return Err(std::io::Error::new(
427 std::io::ErrorKind::InvalidInput,
428 "Seek from current exceeds current position",
429 ));
430 }
431 self.pos.saturating_sub((-offset) as usize)
432 } else {
433 self.pos + offset as usize
434 }
435 }
436 };
437 self.pos = new_pos;
438 Ok(self.pos as u64)
439 }
440
441 fn stream_position(&mut self) -> std::io::Result<u64> {
442 Ok(self.pos as u64)
443 }
444}