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 + Send + Sync>> {
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 + Send + Sync>> {
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<'a>(
69 &self,
70 reader: Box<dyn ReadSeek + Send + Sync + 'a>,
71 _filename: &str,
72 _encoding: Encoding,
73 archive_encoding: Encoding,
74 config: &ExtraConfig,
75 _archive: Option<&Box<dyn Script>>,
76 ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
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
107#[derive(Debug)]
108struct Entry<T: Read + Seek + std::fmt::Debug> {
109 header: CrmFileHeader,
110 reader: Arc<Mutex<T>>,
111 pos: usize,
112 script_type: Option<ScriptType>,
113}
114
115impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for Entry<T> {
116 fn name(&self) -> &str {
117 &self.header.name
118 }
119
120 fn size(&self) -> Option<u64> {
121 Some(self.header.size as u64)
122 }
123
124 fn script_type(&self) -> Option<&ScriptType> {
125 self.script_type.as_ref()
126 }
127
128 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
129 Ok(Box::new(self))
130 }
131}
132
133impl<T: Read + Seek + std::fmt::Debug> Read for Entry<T> {
134 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
135 let mut reader = self.reader.lock().map_err(|e| {
136 std::io::Error::new(
137 std::io::ErrorKind::Other,
138 format!("Failed to lock mutex: {}", e),
139 )
140 })?;
141 reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
142 let bytes_read = buf.len().min(self.header.size as usize - self.pos);
143 if bytes_read == 0 {
144 return Ok(0);
145 }
146 let bytes_read = reader.read(&mut buf[..bytes_read])?;
147 self.pos += bytes_read;
148 Ok(bytes_read)
149 }
150}
151
152impl<T: Read + Seek + std::fmt::Debug> Seek for Entry<T> {
153 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
154 let new_pos = match pos {
155 SeekFrom::Start(offset) => offset as usize,
156 SeekFrom::End(offset) => {
157 if offset < 0 {
158 if (-offset) as usize > self.header.size as usize {
159 return Err(std::io::Error::new(
160 std::io::ErrorKind::InvalidInput,
161 "Seek from end exceeds file length",
162 ));
163 }
164 self.header.size as usize - (-offset) as usize
165 } else {
166 self.header.size as usize + offset as usize
167 }
168 }
169 SeekFrom::Current(offset) => {
170 if offset < 0 {
171 if (-offset) as usize > self.pos {
172 return Err(std::io::Error::new(
173 std::io::ErrorKind::InvalidInput,
174 "Seek from current exceeds current position",
175 ));
176 }
177 self.pos.saturating_sub((-offset) as usize)
178 } else {
179 self.pos + offset as usize
180 }
181 }
182 };
183 self.pos = new_pos;
184 Ok(self.pos as u64)
185 }
186
187 fn stream_position(&mut self) -> std::io::Result<u64> {
188 Ok(self.pos as u64)
189 }
190}
191
192#[derive(Debug)]
193pub struct CrmArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
195 reader: Arc<Mutex<T>>,
196 entries: Vec<CrmFileHeader>,
197 _mark: std::marker::PhantomData<&'b ()>,
198}
199
200impl<'b, T: Read + Seek + std::fmt::Debug + 'b> CrmArchive<'b, T> {
201 pub fn new(mut reader: T, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
207 let mut magic = [0u8; 4];
208 reader.read_exact(&mut magic)?;
209 if &magic != b"CRXB" {
210 return Err(anyhow::anyhow!("Invalid CRM archive magic: {:?}", magic));
211 }
212 reader.seek_relative(4)?;
213 let count = reader.read_u32()? as usize;
214 reader.seek_relative(4)?;
215 let mut entries = Vec::with_capacity(count);
216 let file_len = reader.stream_length()?;
217 let mut offset_map = BTreeMap::new();
218 for _ in 0..count {
219 let offset = reader.read_u32()?;
220 reader.seek_relative(4)?;
221 let name = reader.read_fstring(0x18, encoding, true)?;
222 offset_map.insert(offset, name);
223 }
224 let mut next_iter = offset_map.keys().skip(1);
225 for (offset, name) in &offset_map {
226 let size = if let Some(next) = next_iter.next() {
227 *next
228 } else {
229 file_len as u32
230 } - offset;
231 entries.push(CrmFileHeader {
232 offset: *offset,
233 size,
234 name: name.clone(),
235 });
236 }
237 Ok(Self {
238 reader: Arc::new(Mutex::new(reader)),
239 entries,
240 _mark: std::marker::PhantomData,
241 })
242 }
243}
244
245impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script for CrmArchive<'b, T> {
246 fn default_output_script_type(&self) -> OutputScriptType {
247 OutputScriptType::Json
248 }
249
250 fn default_format_type(&self) -> FormatOptions {
251 FormatOptions::None
252 }
253
254 fn is_archive(&self) -> bool {
255 true
256 }
257
258 fn iter_archive_filename<'a>(
259 &'a self,
260 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
261 Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone()))))
262 }
263
264 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
265 Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
266 }
267
268 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
269 if index >= self.entries.len() {
270 return Err(anyhow::anyhow!(
271 "Index out of bounds: {} (max: {})",
272 index,
273 self.entries.len()
274 ));
275 }
276 let entry = &self.entries[index];
277 let mut entry = Entry {
278 header: entry.clone(),
279 reader: self.reader.clone(),
280 pos: 0,
281 script_type: None,
282 };
283 let mut buf = [0; 32];
284 let readed = match entry.read(&mut buf) {
285 Ok(readed) => readed,
286 Err(e) => {
287 return Err(anyhow::anyhow!(
288 "Failed to read entry '{}': {}",
289 entry.header.name,
290 e
291 ));
292 }
293 };
294 entry.pos = 0;
295 entry.script_type = detect_script_type(&buf, readed, &entry.header.name);
296 Ok(Box::new(entry))
297 }
298}
299
300fn detect_script_type(_buf: &[u8], _buf_len: usize, _filename: &str) -> Option<ScriptType> {
301 #[cfg(feature = "circus-img")]
302 if _buf_len >= 4 && _buf.starts_with(b"CRXG") {
303 return Some(ScriptType::CircusCrx);
304 }
305 #[cfg(feature = "circus-img")]
306 if _buf_len >= 4 && _buf.starts_with(b"CRXD") {
307 return Some(ScriptType::CircusCrxd);
308 }
309 None
310}