mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-07-08 01:31:53 +08:00
Compare commits
2 Commits
fb07e46caa
...
29d08a6935
| Author | SHA1 | Date | |
|---|---|---|---|
| 29d08a6935 | |||
| 1f46cc8cc7 |
17
AGENTS.md
17
AGENTS.md
@@ -16,3 +16,20 @@ panic only allowed in main.rs , args.rs and tests.
|
||||
- `files.rs` - Path utilities to collect inputs, filter by known script or archive extensions, stream stdin/stdout, and sanitize Windows file names.
|
||||
- `struct_pack.rs` - Traits plus blanket implementations for binary pack/unpack backed by the `msg_tool_macro` crate; used when codecs read or write structured data.
|
||||
- Feature-gated helpers such as `bit_stream.rs` or `threadpool.rs` stay under the same module; enable them via the matching `utils-*` features in `Cargo.toml`.
|
||||
|
||||
## Command line tools
|
||||
Use UNIX tools to list directory contents, search text, and compare files. For example: `ls`, `find`, `grep`.
|
||||
POWERHSELL COMMANDS ARE NOT ALLOWED.
|
||||
|
||||
## IO Extensions (`src/ext/io.rs`)
|
||||
For endian-aware operation, default to little-endian, unless `_be` suffix is used. `_le` suffix are not used in the code.
|
||||
- **`Peek` (for `Read + Seek`)** – adds non-destructive read helpers: generic `peek`/`peek_at`, typed endian-aware primitives (`peek_u8`…`peek_i128_be`), string readers (`peek_cstring`, `peek_fstring`, UTF-16 helpers), struct loaders via `StructUnpack` (`read_struct`, `read_struct_vec`), and assertions (`peek_and_equal`).
|
||||
- **`CPeek` (thread-safe peek facade)** – mirrors `Peek` on shared references with exact/offset variants, typed primitives, string helpers, and matchers; implemented for `Mutex<T: Peek>`, in-memory readers, and writers.
|
||||
- **`ReadExt` (for `Read`)** – provides endian-aware reads for integers/floats, C/fixed/UTF-16 string loaders using `Encoding`, vector reads (`read_exact_vec`), and equality assertions (`read_and_equal`).
|
||||
- **`WriteExt` (for `Write`)** – exposes endian-aware writers for integers, signed values, floats, C-strings, plus `write_struct` delegating to `StructPack` with encoding awareness.
|
||||
- **`WriteAt` (for `Write + Seek`)** – writes at absolute offsets with typed wrappers (`write_u8_at`…`write_i128_be_at`) and C-string support, restoring the original cursor after each call.
|
||||
- **`SeekExt` (for `Seek`)** – offers `stream_length` (length without disturbing position) and `align` (rounds the cursor up to a power-of-two boundary).
|
||||
- **`MemReader` / `MemReaderRef`** – byte-slice backed readers implementing `Read`, `Seek`, `Peek`, and `CPeek`; support cloning, EOF checks, borrowing via `to_ref`, and safe position management.
|
||||
- **`MemWriter`** – growable in-memory writer implementing `Write`, `Seek`, and `CPeek`; supports constructing from/into `Vec<u8>` and borrowing via `to_ref`.
|
||||
- **`StreamRegion<T: Seek>`** – wraps a stream segment with bounded `Read`/`Seek`, keeping track of local position and guarding seeks outside the declared range.
|
||||
- **`BinaryPatcher<R, W, A, O>`** – coordinates incremental patching by copying sections, mapping offsets between old/new layouts, replacing ranges with provided writers, and patching values/addresses through user-supplied address↔offset closures.
|
||||
|
||||
@@ -47,6 +47,29 @@ fn check_is_end_quote_or_symbol(segs: &[&str], pos: usize) -> bool {
|
||||
QUOTE_LIST.iter().any(|(_, close)| d == *close) || BREAK_SENTENCE_SYMBOLS.contains(&d)
|
||||
}
|
||||
|
||||
fn check_is_start_quote(s: &str) -> bool {
|
||||
QUOTE_LIST.iter().any(|(open, _)| s == *open)
|
||||
}
|
||||
|
||||
fn take_trailing_start_quotes(buffer: &mut String) -> String {
|
||||
let (collected, trailing) = {
|
||||
let mut collected = buffer.graphemes(true).collect::<Vec<_>>();
|
||||
let mut trailing = Vec::new();
|
||||
while let Some(&last) = collected.last() {
|
||||
if check_is_start_quote(last) {
|
||||
collected.pop();
|
||||
trailing.push(last);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
trailing.reverse();
|
||||
(collected.concat(), trailing.concat())
|
||||
};
|
||||
*buffer = collected;
|
||||
trailing
|
||||
}
|
||||
|
||||
#[cfg(feature = "jieba")]
|
||||
fn check_chinese_word_is_break(segs: &[&str], pos: usize, jieba: &Jieba) -> bool {
|
||||
let s = segs.join("");
|
||||
@@ -282,8 +305,16 @@ impl FixedFormatter {
|
||||
}
|
||||
}
|
||||
if let Some(pos) = break_pos {
|
||||
let remaining = segs[pos..].concat().trim_start().to_string();
|
||||
result = segs[..pos].concat();
|
||||
let mut head = segs[..pos].concat();
|
||||
let mut remaining = segs[pos..].concat();
|
||||
if self.break_with_sentence {
|
||||
let trailing = take_trailing_start_quotes(&mut head);
|
||||
if !trailing.is_empty() {
|
||||
remaining.insert_str(0, &trailing);
|
||||
}
|
||||
}
|
||||
let remaining = remaining.trim_start().to_string();
|
||||
result = head;
|
||||
result.push('\n');
|
||||
current_length = 0;
|
||||
if first_line {
|
||||
@@ -303,6 +334,11 @@ impl FixedFormatter {
|
||||
main_content.clear();
|
||||
pre_is_lf = true;
|
||||
} else {
|
||||
let trailing = if self.break_with_sentence {
|
||||
take_trailing_start_quotes(&mut result)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
result.push('\n');
|
||||
current_length = 0;
|
||||
if first_line {
|
||||
@@ -318,6 +354,11 @@ impl FixedFormatter {
|
||||
current_length += 1;
|
||||
}
|
||||
main_content.clear();
|
||||
if !trailing.is_empty() {
|
||||
result.push_str(&trailing);
|
||||
current_length += trailing.graphemes(true).count();
|
||||
main_content.push_str(&trailing);
|
||||
}
|
||||
pre_is_lf = true;
|
||||
}
|
||||
} else if !self.break_words
|
||||
@@ -347,8 +388,15 @@ impl FixedFormatter {
|
||||
|
||||
// If we found a good break point, move content after it to next line
|
||||
if let Some(pos) = break_pos {
|
||||
let remaining = result[pos..].trim_start().to_string();
|
||||
let mut remaining = result[pos..].to_string();
|
||||
result.truncate(pos);
|
||||
if self.break_with_sentence {
|
||||
let trailing = take_trailing_start_quotes(&mut result);
|
||||
if !trailing.is_empty() {
|
||||
remaining.insert_str(0, &trailing);
|
||||
}
|
||||
}
|
||||
let remaining = remaining.trim_start().to_string();
|
||||
result.push('\n');
|
||||
current_length = 0;
|
||||
if first_line {
|
||||
@@ -368,6 +416,11 @@ impl FixedFormatter {
|
||||
main_content.clear();
|
||||
pre_is_lf = true;
|
||||
} else {
|
||||
let trailing = if self.break_with_sentence {
|
||||
take_trailing_start_quotes(&mut result)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
result.push('\n');
|
||||
current_length = 0;
|
||||
if first_line {
|
||||
@@ -383,6 +436,11 @@ impl FixedFormatter {
|
||||
current_length += 1;
|
||||
}
|
||||
main_content.clear();
|
||||
if !trailing.is_empty() {
|
||||
result.push_str(&trailing);
|
||||
current_length += trailing.graphemes(true).count();
|
||||
main_content.push_str(&trailing);
|
||||
}
|
||||
pre_is_lf = true;
|
||||
}
|
||||
} else if self
|
||||
@@ -421,8 +479,16 @@ impl FixedFormatter {
|
||||
let segs = result.graphemes(true).collect::<Vec<_>>();
|
||||
let remain_count = i - pos;
|
||||
let pos = segs.len() - remain_count;
|
||||
let remaining = segs[pos..].concat().trim_start().to_string();
|
||||
result = segs[..pos].concat();
|
||||
let mut head = segs[..pos].concat();
|
||||
let mut remaining = segs[pos..].concat();
|
||||
if self.break_with_sentence {
|
||||
let trailing = take_trailing_start_quotes(&mut head);
|
||||
if !trailing.is_empty() {
|
||||
remaining.insert_str(0, &trailing);
|
||||
}
|
||||
}
|
||||
let remaining = remaining.trim_start().to_string();
|
||||
result = head;
|
||||
result.push('\n');
|
||||
current_length = 0;
|
||||
if first_line {
|
||||
@@ -442,6 +508,11 @@ impl FixedFormatter {
|
||||
main_content.clear();
|
||||
pre_is_lf = true;
|
||||
} else {
|
||||
let trailing = if self.break_with_sentence {
|
||||
take_trailing_start_quotes(&mut result)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
result.push('\n');
|
||||
current_length = 0;
|
||||
if first_line {
|
||||
@@ -457,10 +528,20 @@ impl FixedFormatter {
|
||||
current_length += 1;
|
||||
}
|
||||
main_content.clear();
|
||||
if !trailing.is_empty() {
|
||||
result.push_str(&trailing);
|
||||
current_length += trailing.graphemes(true).count();
|
||||
main_content.push_str(&trailing);
|
||||
}
|
||||
pre_is_lf = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let trailing = if self.break_with_sentence {
|
||||
take_trailing_start_quotes(&mut result)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
result.push('\n');
|
||||
current_length = 0;
|
||||
if first_line {
|
||||
@@ -476,6 +557,11 @@ impl FixedFormatter {
|
||||
current_length += 1;
|
||||
}
|
||||
main_content.clear();
|
||||
if !trailing.is_empty() {
|
||||
result.push_str(&trailing);
|
||||
current_length += trailing.graphemes(true).count();
|
||||
main_content.push_str(&trailing);
|
||||
}
|
||||
pre_is_lf = true;
|
||||
}
|
||||
}
|
||||
@@ -660,6 +746,15 @@ fn test_format() {
|
||||
"这打断测试哦测试一下\n。。"
|
||||
);
|
||||
|
||||
let formatter5 = FixedFormatter::builder(10)
|
||||
.break_words(false)
|
||||
.insert_fullwidth_space_at_line_start(true)
|
||||
.break_with_sentence(true);
|
||||
assert_eq!(
|
||||
formatter5.format("「一二三四『whatthe』"),
|
||||
"「一二三四\n\u{3000}『whatthe』"
|
||||
);
|
||||
|
||||
let real_break_formatter = FixedFormatter::builder(27)
|
||||
.break_words(false)
|
||||
.break_with_sentence(true);
|
||||
@@ -668,6 +763,12 @@ fn test_format() {
|
||||
"「他们就是想和阳见待在一个社团,\n在里面表现表现、耍耍帅,这样不就和她套上近乎了嘛!算盘\n珠子都打到我脸上了……」"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
real_break_formatter
|
||||
.format("「在英山的话或许可以看看『moon river』『Lavir』或是『Patisserie Yuzuru』」"),
|
||||
"「在英山的话或许可以看看『moon river』\n『Lavir』或是『Patisserie Yuzuru\n』」"
|
||||
);
|
||||
|
||||
#[cfg(feature = "circus")]
|
||||
{
|
||||
let circus_formatter = FixedFormatter::builder(10).typ(Some(ScriptType::Circus));
|
||||
|
||||
@@ -462,4 +462,20 @@ impl ScriptInfo {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_encrypted_message(&self, opcode: u8) -> bool {
|
||||
self.encstr.its(opcode)
|
||||
}
|
||||
|
||||
pub fn is_unencrypted_message(&self, opcode: u8) -> bool {
|
||||
opcode == self.optunenc
|
||||
}
|
||||
|
||||
pub fn is_message_opcode(&self, opcode: u8) -> bool {
|
||||
self.is_encrypted_message(opcode) || self.is_unencrypted_message(opcode)
|
||||
}
|
||||
|
||||
pub fn is_name_opcode(&self, opcode: u8) -> bool {
|
||||
opcode == self.nameopcode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ impl Script for CircusMesScript {
|
||||
fn import_messages<'a>(
|
||||
&'a self,
|
||||
messages: Vec<Message>,
|
||||
mut writer: Box<dyn WriteSeek + 'a>,
|
||||
writer: Box<dyn WriteSeek + 'a>,
|
||||
_filename: &str,
|
||||
encoding: Encoding,
|
||||
replacement: Option<&'a ReplacementTable>,
|
||||
@@ -292,122 +292,103 @@ impl Script for CircusMesScript {
|
||||
crate::COUNTER.inc_warning();
|
||||
}
|
||||
}
|
||||
match replacement {
|
||||
Some(repl) => {
|
||||
for (k, v) in repl.map.iter() {
|
||||
repls.push((k.to_string(), v.to_string()));
|
||||
}
|
||||
if let Some(repl) = replacement {
|
||||
for (k, v) in repl.map.iter() {
|
||||
repls.push((k.to_string(), v.to_string()));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
let mut buffer = Vec::with_capacity(self.data.len());
|
||||
buffer.extend_from_slice(&self.data[..self.asm_bin_offset]);
|
||||
let mut nmes = Vec::with_capacity(messages.len());
|
||||
for m in messages {
|
||||
nmes.insert(0, m);
|
||||
}
|
||||
let mut mes = nmes.pop();
|
||||
let mut block_count = 0;
|
||||
for token in self.tokens.iter() {
|
||||
|
||||
let source = MemReaderRef::new(&self.data);
|
||||
let mut patcher = BinaryPatcher::new(source, writer, |pos| Ok(pos), |pos| Ok(pos))?;
|
||||
|
||||
let mut pending_messages: Vec<Message> = messages.into_iter().rev().collect();
|
||||
let mut current_message = pending_messages.pop();
|
||||
let mut block_updates: Vec<(u64, u32)> = Vec::new();
|
||||
let mut block_index = 0usize;
|
||||
|
||||
for token in &self.tokens {
|
||||
let token_start = (self.asm_bin_offset + token.offset) as u64;
|
||||
patcher.copy_up_to(token_start)?;
|
||||
|
||||
if !self.is_new_ver {
|
||||
let count = buffer.len() as u32;
|
||||
let offset = count - self.asm_bin_offset as u32 + 2;
|
||||
buffer[self.blocks_offset + block_count * 4
|
||||
..self.blocks_offset + block_count * 4 + 4]
|
||||
.copy_from_slice(&offset.to_le_bytes());
|
||||
block_count += 1;
|
||||
let block_offset = (self.blocks_offset + block_index * 4) as u64;
|
||||
let new_offset = patcher.map_offset(token_start)?;
|
||||
let offset_value = (new_offset - self.asm_bin_offset as u64 + 2) as u32;
|
||||
block_updates.push((block_offset, offset_value));
|
||||
block_index += 1;
|
||||
}
|
||||
if self.info.encstr.its(token.value) {
|
||||
if mes.is_none() {
|
||||
mes = nmes.pop();
|
||||
if mes.is_none() {
|
||||
|
||||
if self.info.is_message_opcode(token.value) {
|
||||
if current_message.is_none() {
|
||||
current_message = pending_messages.pop();
|
||||
if current_message.is_none() {
|
||||
return Err(anyhow::anyhow!("No more messages to import"));
|
||||
}
|
||||
}
|
||||
let mut s = if token.value == self.info.nameopcode {
|
||||
match mes.as_mut().unwrap().name.take() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
let t = mes.as_ref().unwrap().message.clone();
|
||||
mes = None;
|
||||
t
|
||||
|
||||
let mut text = {
|
||||
let message = current_message.as_mut().unwrap();
|
||||
if self.info.is_name_opcode(token.value) {
|
||||
match message.name.take() {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
let msg = message.message.clone();
|
||||
current_message = None;
|
||||
msg
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let msg = message.message.clone();
|
||||
current_message = None;
|
||||
msg
|
||||
}
|
||||
} else {
|
||||
let t = mes.as_ref().unwrap().message.clone();
|
||||
mes = None;
|
||||
t
|
||||
};
|
||||
for i in repls.iter() {
|
||||
s = s.replace(i.0.as_str(), i.1.as_str());
|
||||
|
||||
for (from, to) in &repls {
|
||||
text = text.replace(from, to);
|
||||
}
|
||||
let mut text = encode_string(encoding, &s, false)?;
|
||||
if text.contains(&self.info.deckey) {
|
||||
eprintln!(
|
||||
"Warning: text contains deckey 0x{:02X}, text may be truncated: {}",
|
||||
self.info.deckey, s,
|
||||
);
|
||||
crate::COUNTER.inc_warning();
|
||||
|
||||
let mut token_bytes = Vec::with_capacity(text.len() + 2);
|
||||
token_bytes.push(token.value);
|
||||
let mut encoded = encode_string(encoding, &text, false)?;
|
||||
if self.info.is_encrypted_message(token.value) {
|
||||
if encoded.contains(&self.info.deckey) {
|
||||
eprintln!(
|
||||
"Warning: text contains deckey 0x{:02X}, text may be truncated: {}",
|
||||
self.info.deckey, text,
|
||||
);
|
||||
crate::COUNTER.inc_warning();
|
||||
}
|
||||
for b in &mut encoded {
|
||||
*b = (*b).overflowing_sub(self.info.deckey).0;
|
||||
}
|
||||
}
|
||||
buffer.push(token.value);
|
||||
for t in text.iter_mut() {
|
||||
*t = (*t).overflowing_sub(self.info.deckey).0;
|
||||
}
|
||||
buffer.extend_from_slice(&text);
|
||||
buffer.push(0x00);
|
||||
token_bytes.extend_from_slice(&encoded);
|
||||
token_bytes.push(0x00);
|
||||
patcher.replace_bytes(token.length as u64, &token_bytes)?;
|
||||
continue;
|
||||
}
|
||||
if token.value == self.info.optunenc {
|
||||
if mes.is_none() {
|
||||
mes = nmes.pop();
|
||||
if mes.is_none() {
|
||||
return Err(anyhow::anyhow!("No more messages to import"));
|
||||
}
|
||||
}
|
||||
let mut s = if token.value == self.info.nameopcode {
|
||||
match mes.as_mut().unwrap().name.take() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
let t = mes.as_ref().unwrap().message.clone();
|
||||
mes = None;
|
||||
t
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let t = mes.as_ref().unwrap().message.clone();
|
||||
mes = None;
|
||||
t
|
||||
};
|
||||
for i in repls.iter() {
|
||||
s = s.replace(i.0.as_str(), i.1.as_str());
|
||||
}
|
||||
buffer.push(token.value);
|
||||
let text = encode_string(encoding, &s, false)?;
|
||||
buffer.extend_from_slice(&text);
|
||||
buffer.push(0x00);
|
||||
continue;
|
||||
|
||||
if self.is_new_ver && (token.value == 0x03 || token.value == 0x04) {
|
||||
let block_offset = (self.blocks_offset + block_index * 4) as u64;
|
||||
let original_block = patcher.input.cpeek_u32_at(block_offset)?;
|
||||
let new_offset = patcher.map_offset(token_start)?;
|
||||
let offset = (new_offset - self.asm_bin_offset as u64 + token.length as u64) as u32;
|
||||
let value = (original_block & (0xFF << 0x18)) | offset;
|
||||
block_updates.push((block_offset, value));
|
||||
block_index += 1;
|
||||
}
|
||||
if self.is_new_ver && (token.value == 0x3 || token.value == 0x4) {
|
||||
let count = buffer.len() as u32;
|
||||
let offset = count - self.asm_bin_offset as u32 + token.length as u32;
|
||||
let block = u32::from_le_bytes(
|
||||
buffer[self.blocks_offset + block_count * 4
|
||||
..self.blocks_offset + block_count * 4 + 4]
|
||||
.try_into()?,
|
||||
);
|
||||
let block = (block & (0xFF << 0x18)) | offset;
|
||||
buffer[self.blocks_offset + block_count * 4
|
||||
..self.blocks_offset + block_count * 4 + 4]
|
||||
.copy_from_slice(&block.to_le_bytes());
|
||||
block_count += 1;
|
||||
}
|
||||
let len = std::cmp::min(
|
||||
self.asm_bin_offset + token.offset + token.length,
|
||||
self.data.len(),
|
||||
);
|
||||
buffer.extend_from_slice(&self.data[self.asm_bin_offset + token.offset..len]);
|
||||
|
||||
let token_end = token_start + token.length as u64;
|
||||
patcher.copy_up_to(token_end)?;
|
||||
}
|
||||
writer.write_all(&buffer)?;
|
||||
|
||||
patcher.copy_up_to(self.data.len() as u64)?;
|
||||
|
||||
for (offset, value) in block_updates {
|
||||
patcher.patch_u32(offset, value)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user