From c97f59f43bca7c3a96a2c5e48aa8998e086b3612 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Thu, 26 Mar 2026 09:18:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=20Psb=20Version=204=20?= =?UTF-8?q?=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ext/psb.rs | 89 ++++++++++++++++++++++++++++++++++++- src/scripts/emote/psb.rs | 14 +----- src/scripts/kirikiri/scn.rs | 8 ++-- 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/src/ext/psb.rs b/src/ext/psb.rs index 49b1c89..e11992a 100644 --- a/src/ext/psb.rs +++ b/src/ext/psb.rs @@ -1,9 +1,12 @@ //!Extensions for emote_psb crate. use crate::ext::io::*; +use adler::Adler32; use anyhow::Result; +use emote_psb::PsbError; use emote_psb::PsbFile; use emote_psb::PsbReader; use emote_psb::PsbRefs; +use emote_psb::PsbWriter; use emote_psb::VirtualPsb; use emote_psb::header::PsbHeader; use emote_psb::offsets::{PsbOffsets, PsbResourcesOffset, PsbStringOffset}; @@ -21,7 +24,7 @@ use serde::ser::SerializeStruct; use serde::{Deserialize, Serialize}; use std::cmp::PartialEq; use std::collections::{BTreeMap, HashMap}; -use std::io::{Read, Seek, SeekFrom}; +use std::io::{Read, Seek, SeekFrom, Write}; use std::ops::{Index, IndexMut}; #[cfg(feature = "json")] @@ -1257,6 +1260,90 @@ impl VirtualPsbExt for VirtualPsb { } } +/// Trait to extend PSB writer behavior. +pub trait PsbWriterExt { + /// Writes PSB with a v4-compatible resource layout. + fn finish_v4(self, stream: T) -> std::result::Result; +} + +impl PsbWriterExt for VirtualPsb { + fn finish_v4(self, mut stream: T) -> std::result::Result { + let file_start = stream.seek(SeekFrom::Current(0))?; + + let (header, resources, extra, root) = self.unwrap(); + + stream.write_u32(PSB_SIGNATURE)?; + header.write_bytes(&mut stream)?; + + let offsets_end_pos = stream.seek(SeekFrom::Current(0))? - file_start; + stream.write_u32(0)?; + + let offset_start_pos = stream.seek(SeekFrom::Current(0))? - file_start; + let mut offsets = PsbOffsets::default(); + + offsets.write_bytes(header.version, &mut stream)?; + let offsets_end = stream.seek(SeekFrom::Current(0))? - file_start; + + let refs = { + let mut names = Vec::new(); + let mut strings = Vec::new(); + + root.collect_names(&mut names); + root.collect_strings(&mut strings); + + names.sort(); + strings.sort(); + + PsbRefs::new(names, strings) + }; + + offsets.name_offset = (stream.seek(SeekFrom::Current(0))? - file_start) as u32; + PsbWriter::write_names(refs.names(), &mut stream)?; + + offsets.entry_point = (stream.seek(SeekFrom::Current(0))? - file_start) as u32; + PsbValue::Object(root).write_bytes_refs(&mut stream, &refs)?; + + let (_, string_offsets) = PsbWriter::write_strings(refs.strings(), &mut stream)?; + offsets.strings = string_offsets; + + // For v4, write extra resources before normal resources to keep compatibility + // with tools expecting the FreeMote layout. + if header.version > 3 { + let (_, extra_offsets) = PsbWriter::write_resources(&extra, &mut stream)?; + offsets.extra = Some(extra_offsets); + } + + let (_, res_offsets) = PsbWriter::write_resources(&resources, &mut stream)?; + offsets.resources = res_offsets; + + let file_end = stream.seek(SeekFrom::Current(0))?; + + stream.seek(SeekFrom::Start(offsets_end_pos))?; + stream.write_u32(offsets_end as u32)?; + + if header.version > 2 { + let mut adler = Adler32::new(); + + adler.write_slice(&(offset_start_pos as u32).to_le_bytes()); + adler.write_slice(&offsets.name_offset.to_le_bytes()); + adler.write_slice(&offsets.strings.offset_pos.to_le_bytes()); + adler.write_slice(&offsets.strings.data_pos.to_le_bytes()); + adler.write_slice(&offsets.resources.offset_pos.to_le_bytes()); + adler.write_slice(&offsets.resources.lengths_pos.to_le_bytes()); + adler.write_slice(&offsets.resources.data_pos.to_le_bytes()); + adler.write_slice(&offsets.entry_point.to_le_bytes()); + + offsets.checksum = Some(adler.checksum()); + } + + stream.seek(SeekFrom::Start(offset_start_pos))?; + offsets.write_bytes(header.version, &mut stream)?; + stream.seek(SeekFrom::Start(file_end))?; + + Ok(file_end - file_start) + } +} + /// Trait to extend PSB reader functionality. pub trait PsbReaderExt { /// Opens a PSB v2 file from a stream, handling other formats like LZ4 compression. diff --git a/src/scripts/emote/psb.rs b/src/scripts/emote/psb.rs index 211fe0c..7d5713f 100644 --- a/src/scripts/emote/psb.rs +++ b/src/scripts/emote/psb.rs @@ -628,14 +628,6 @@ fn create_file<'a>( let resources: Vec = serde_json::from_str(&data["resources"].dump())?; let extra_resources: Vec = serde_json::from_str(&data["extra_resources"].dump())?; let mut psb = VirtualPsbFixed::with_json(&data)?; - if psb.header().version > 3 { - eprintln!( - "Warning: PSB version {} is higher than 3, downgrading to 3. Some features may not be supported.", - psb.header().version - ); - crate::COUNTER.inc_warning(); - psb.header_mut().version = 3; - } psb.header_mut().encryption = 0; // We don't support encryption. let folder_path = { let mut pb = std::path::PathBuf::from(custom_filename); @@ -651,9 +643,7 @@ fn create_file<'a>( psb.extra_mut().push(res); } let psb = psb.to_psb(false); - let psb_writer = PsbWriter::new(psb, &mut writer); - psb_writer - .finish() - .map_err(|e| anyhow::anyhow!("Failed to write psb: {:?}", e))?; + psb.finish_v4(&mut writer) + .map_err(|e| anyhow::anyhow!("Failed to write PSB file: {:?}", e))?; Ok(()) } diff --git a/src/scripts/kirikiri/scn.rs b/src/scripts/kirikiri/scn.rs index 2e7409f..8e00a72 100644 --- a/src/scripts/kirikiri/scn.rs +++ b/src/scripts/kirikiri/scn.rs @@ -7,7 +7,7 @@ use crate::scripts::base::*; use crate::types::*; use crate::utils::encoding::*; use anyhow::Result; -use emote_psb::{PsbReader, PsbWriter}; +use emote_psb::PsbReader; use fancy_regex::Regex; use std::collections::{HashMap, HashSet}; use std::io::{Read, Seek}; @@ -838,8 +838,7 @@ impl Script for ScnScript { return Err(anyhow::anyhow!("Some messages were not processed.")); } let psb = psb.to_psb(true); - let writer = PsbWriter::new(psb, file); - writer.finish().map_err(|e| { + psb.finish_v4(file).map_err(|e| { anyhow::anyhow!("Failed to write PSB to file {}: {:?}", self.filename, e) })?; Ok(()) @@ -879,8 +878,7 @@ impl Script for ScnScript { psb.from_json(&json)?; psb.to_psb(true) }; - let writer = PsbWriter::new(psb, file); - writer.finish().map_err(|e| { + psb.finish_v4(file).map_err(|e| { anyhow::anyhow!("Failed to write PSB to file {}: {:?}", self.filename, e) })?; Ok(())