msg_tool\scripts\circus\archive/
crm.rs

1//! Circus Image Archive File (.crm)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::Result;
6use std::collections::BTreeMap;
7use std::io::{Read, Seek, SeekFrom};
8use std::sync::{Arc, Mutex};
9
10#[derive(Debug)]
11/// Circus CRM Archive Builder
12pub struct CrmArchiveBuilder {}
13
14impl CrmArchiveBuilder {
15    /// Creates a new instance of `CrmArchiveBuilder`.
16    pub fn new() -> Self {
17        Self {}
18    }
19}
20
21impl ScriptBuilder for CrmArchiveBuilder {
22    fn default_encoding(&self) -> Encoding {
23        Encoding::Cp932
24    }
25
26    fn default_archive_encoding(&self) -> Option<Encoding> {
27        Some(Encoding::Cp932)
28    }
29
30    fn build_script(
31        &self,
32        data: Vec<u8>,
33        _filename: &str,
34        _encoding: Encoding,
35        archive_encoding: Encoding,
36        config: &ExtraConfig,
37        _archive: Option<&Box<dyn Script>>,
38    ) -> Result<Box<dyn Script>> {
39        Ok(Box::new(CrmArchive::new(
40            MemReader::new(data),
41            archive_encoding,
42            config,
43        )?))
44    }
45
46    fn build_script_from_file(
47        &self,
48        filename: &str,
49        _encoding: Encoding,
50        archive_encoding: Encoding,
51        config: &ExtraConfig,
52        _archive: Option<&Box<dyn Script>>,
53    ) -> Result<Box<dyn Script>> {
54        if filename == "-" {
55            let data = crate::utils::files::read_file(filename)?;
56            Ok(Box::new(CrmArchive::new(
57                MemReader::new(data),
58                archive_encoding,
59                config,
60            )?))
61        } else {
62            let f = std::fs::File::open(filename)?;
63            let reader = std::io::BufReader::new(f);
64            Ok(Box::new(CrmArchive::new(reader, archive_encoding, config)?))
65        }
66    }
67
68    fn build_script_from_reader(
69        &self,
70        reader: Box<dyn ReadSeek>,
71        _filename: &str,
72        _encoding: Encoding,
73        archive_encoding: Encoding,
74        config: &ExtraConfig,
75        _archive: Option<&Box<dyn Script>>,
76    ) -> Result<Box<dyn Script>> {
77        Ok(Box::new(CrmArchive::new(reader, archive_encoding, config)?))
78    }
79
80    fn extensions(&self) -> &'static [&'static str] {
81        &["crm"]
82    }
83
84    fn script_type(&self) -> &'static ScriptType {
85        &ScriptType::CircusCrm
86    }
87
88    fn is_archive(&self) -> bool {
89        true
90    }
91
92    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
93        if buf_len >= 4 && buf.starts_with(b"CRXB") {
94            return Some(10);
95        }
96        None
97    }
98}
99
100#[derive(Debug, Clone)]
101struct CrmFileHeader {
102    offset: u32,
103    size: u32,
104    name: String,
105}
106
107struct Entry<T: Read + Seek> {
108    header: CrmFileHeader,
109    reader: Arc<Mutex<T>>,
110    pos: usize,
111    script_type: Option<ScriptType>,
112}
113
114impl<T: Read + Seek> ArchiveContent for Entry<T> {
115    fn name(&self) -> &str {
116        &self.header.name
117    }
118
119    fn script_type(&self) -> Option<&ScriptType> {
120        self.script_type.as_ref()
121    }
122}
123
124impl<T: Read + Seek> Read for Entry<T> {
125    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
126        let mut reader = self.reader.lock().map_err(|e| {
127            std::io::Error::new(
128                std::io::ErrorKind::Other,
129                format!("Failed to lock mutex: {}", e),
130            )
131        })?;
132        reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
133        let bytes_read = buf.len().min(self.header.size as usize - self.pos);
134        if bytes_read == 0 {
135            return Ok(0);
136        }
137        let bytes_read = reader.read(&mut buf[..bytes_read])?;
138        self.pos += bytes_read;
139        Ok(bytes_read)
140    }
141}
142
143impl<T: Read + Seek> Seek for Entry<T> {
144    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
145        let new_pos = match pos {
146            SeekFrom::Start(offset) => offset as usize,
147            SeekFrom::End(offset) => {
148                if offset < 0 {
149                    if (-offset) as usize > self.header.size as usize {
150                        return Err(std::io::Error::new(
151                            std::io::ErrorKind::InvalidInput,
152                            "Seek from end exceeds file length",
153                        ));
154                    }
155                    self.header.size as usize - (-offset) as usize
156                } else {
157                    self.header.size as usize + offset as usize
158                }
159            }
160            SeekFrom::Current(offset) => {
161                if offset < 0 {
162                    if (-offset) as usize > self.pos {
163                        return Err(std::io::Error::new(
164                            std::io::ErrorKind::InvalidInput,
165                            "Seek from current exceeds current position",
166                        ));
167                    }
168                    self.pos.saturating_sub((-offset) as usize)
169                } else {
170                    self.pos + offset as usize
171                }
172            }
173        };
174        self.pos = new_pos;
175        Ok(self.pos as u64)
176    }
177
178    fn stream_position(&mut self) -> std::io::Result<u64> {
179        Ok(self.pos as u64)
180    }
181}
182
183#[derive(Debug)]
184/// Circus CRM Archive
185pub struct CrmArchive<T: Read + Seek + std::fmt::Debug> {
186    reader: Arc<Mutex<T>>,
187    entries: Vec<CrmFileHeader>,
188}
189
190impl<T: Read + Seek + std::fmt::Debug> CrmArchive<T> {
191    /// Creates a new `CrmArchive` from a reader.
192    ///
193    /// * `reader` - The reader to read the CRM archive from.
194    /// * `encoding` - The encoding to use for string fields.
195    /// * `config` - Extra configuration options.
196    pub fn new(mut reader: T, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
197        let mut magic = [0u8; 4];
198        reader.read_exact(&mut magic)?;
199        if &magic != b"CRXB" {
200            return Err(anyhow::anyhow!("Invalid CRM archive magic: {:?}", magic));
201        }
202        reader.seek_relative(4)?;
203        let count = reader.read_u32()? as usize;
204        reader.seek_relative(4)?;
205        let mut entries = Vec::with_capacity(count);
206        let file_len = reader.stream_length()?;
207        let mut offset_map = BTreeMap::new();
208        for _ in 0..count {
209            let offset = reader.read_u32()?;
210            reader.seek_relative(4)?;
211            let name = reader.read_fstring(0x18, encoding, true)?;
212            offset_map.insert(offset, name);
213        }
214        let mut next_iter = offset_map.keys().skip(1);
215        for (offset, name) in &offset_map {
216            let size = if let Some(next) = next_iter.next() {
217                *next
218            } else {
219                file_len as u32
220            } - offset;
221            entries.push(CrmFileHeader {
222                offset: *offset,
223                size,
224                name: name.clone(),
225            });
226        }
227        Ok(Self {
228            reader: Arc::new(Mutex::new(reader)),
229            entries,
230        })
231    }
232}
233
234impl<T: Read + Seek + std::fmt::Debug + 'static> Script for CrmArchive<T> {
235    fn default_output_script_type(&self) -> OutputScriptType {
236        OutputScriptType::Json
237    }
238
239    fn default_format_type(&self) -> FormatOptions {
240        FormatOptions::None
241    }
242
243    fn is_archive(&self) -> bool {
244        true
245    }
246
247    fn iter_archive_filename<'a>(
248        &'a self,
249    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
250        Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone()))))
251    }
252
253    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
254        Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
255    }
256
257    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
258        if index >= self.entries.len() {
259            return Err(anyhow::anyhow!(
260                "Index out of bounds: {} (max: {})",
261                index,
262                self.entries.len()
263            ));
264        }
265        let entry = &self.entries[index];
266        let mut entry = Entry {
267            header: entry.clone(),
268            reader: self.reader.clone(),
269            pos: 0,
270            script_type: None,
271        };
272        let mut buf = [0; 32];
273        let readed = match entry.read(&mut buf) {
274            Ok(readed) => readed,
275            Err(e) => {
276                return Err(anyhow::anyhow!(
277                    "Failed to read entry '{}': {}",
278                    entry.header.name,
279                    e
280                ));
281            }
282        };
283        entry.pos = 0;
284        entry.script_type = detect_script_type(&buf, readed, &entry.header.name);
285        Ok(Box::new(entry))
286    }
287}
288
289fn detect_script_type(_buf: &[u8], _buf_len: usize, _filename: &str) -> Option<ScriptType> {
290    #[cfg(feature = "circus-img")]
291    if _buf_len >= 4 && _buf.starts_with(b"CRXG") {
292        return Some(ScriptType::CircusCrx);
293    }
294    #[cfg(feature = "circus-img")]
295    if _buf_len >= 4 && _buf.starts_with(b"CRXD") {
296        return Some(ScriptType::CircusCrxd);
297    }
298    None
299}