msg_tool\scripts\circus\archive/
crm.rs1use 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)]
11pub struct CrmArchiveBuilder {}
13
14impl CrmArchiveBuilder {
15 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)]
184pub 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 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}