5 Commits

Author SHA1 Message Date
3b33dceb61 release 0.2.15 & Update deps 2025-12-13 10:09:33 +08:00
67ea0ff8e6 Allow to load files without case sensitive 2025-12-12 23:01:09 +08:00
552160ec86 Allow to load file with ignore case 2025-12-12 10:50:21 +08:00
1203b25b63 0.2.14 release 2025-12-11 15:39:57 +08:00
3c49e9a991 Add support to import softpal pgd image as normal ge image 2025-12-11 15:39:14 +08:00
8 changed files with 422 additions and 377 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
/patched
*.log
.vscode/
/vendor

611
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "msg_tool"
version = "0.2.13"
version = "0.2.15"
edition = "2024"
repository = "https://github.com/lifegpc/msg-tool"
description = "A command-line tool for exporting, importing, packing, and unpacking script files."
@@ -30,8 +30,8 @@ lazy_static = "1.5.0"
libflac-sys = { version = "0.3", optional = true }
libtlg-rs = { version = "0.2", optional = true, features = ["encode"] }
lz4 = { version = "1.28", optional = true }
markup5ever = { version = "0.35", optional = true }
markup5ever_rcdom = { version = "0.35", optional = true }
markup5ever = { version = "0.36", optional = true }
markup5ever_rcdom = { version = "0.36", optional = true }
memchr = { version = "2.7", optional = true }
mozjpeg = { version = "0.10", optional = true }
msg_tool_macro = { version = "0.2.12" }
@@ -52,7 +52,7 @@ unicode-segmentation = "1.12"
url = { version = "2.5", optional = true }
utf16string = "0.2"
webp = { version = "0.3", default-features = false, optional = true }
xml5ever = { version = "0.35", optional = true }
xml5ever = { version = "0.36", optional = true }
xp3 = { version = "0.3", optional = true}
zstd = { version = "0.13", optional = true }

View File

@@ -99,6 +99,7 @@ impl CrxdImage {
} else {
let mut nf = std::path::PathBuf::from(filename);
nf.set_file_name(name);
nf = crate::utils::files::get_ignorecase_path(&nf)?;
let f = std::fs::File::open(nf)?;
CrxImage::new(std::io::BufReader::new(f), config)?
}

View File

@@ -189,7 +189,8 @@ impl DpakLoader {
let dpak = match self.map.get(dpak) {
Some(d) => d,
None => {
let path = dir.join(dpak);
let mut path = dir.join(dpak);
path = crate::utils::files::get_ignorecase_path(&path)?;
let ndpak = Dpak::new(&path)?;
self.map.insert(dpak.to_string(), ndpak);
self.map.get(dpak).unwrap()

View File

@@ -65,6 +65,7 @@ pub struct Pgd3 {
base_header: PgdGeHeader,
base: ImageData,
diff: ImageData,
fake_compress: bool,
}
impl Pgd3 {
@@ -72,7 +73,7 @@ impl Pgd3 {
mut reader: R,
filename: &str,
encoding: Encoding,
_config: &ExtraConfig,
config: &ExtraConfig,
archive: Option<&Box<dyn Script>>,
) -> Result<Self> {
let mut sig = [0u8; 4];
@@ -89,7 +90,7 @@ impl Pgd3 {
let path = {
let mut pb = std::path::PathBuf::from(filename);
pb.set_file_name(&header.base_name);
pb
crate::utils::files::get_ignorecase_path(&pb)?
};
crate::utils::files::read_file(&path).map_err(|e| {
anyhow::anyhow!("Failed to read base image file '{}': {}", path.display(), e)
@@ -110,6 +111,7 @@ impl Pgd3 {
base_header,
base,
diff,
fake_compress: config.pgd_fake_compress,
})
}
}
@@ -147,6 +149,44 @@ impl Script for Pgd3 {
)?;
Ok(base)
}
fn import_image<'a>(
&'a self,
data: ImageData,
mut file: Box<dyn WriteSeek + 'a>,
) -> Result<()> {
let mut header = PgdGeHeader {
offset_x: self.base_header.offset_x,
offset_y: self.base_header.offset_y,
width: self.base_header.width,
height: self.base_header.height,
canvas_height: self.base_header.canvas_height,
canvas_width: self.base_header.canvas_width,
mode: self.base_header.mode,
_unk: self.base_header._unk,
};
if data.height != header.height {
return Err(anyhow::anyhow!(
"Image height does not match: expected {}, got {}",
header.height,
data.height
));
}
if data.width != header.width {
return Err(anyhow::anyhow!(
"Image width does not match: expected {}, got {}",
header.width,
data.width
));
}
header.mode = 3;
file.write_all(b"GE \0")?;
header.pack(&mut file, false, Encoding::Utf8)?;
PgdWriter::new(data, self.fake_compress)
.with_method(3)
.pack_ge(&mut file)?;
Ok(())
}
}
fn draw_on_img(base: &mut ImageData, diff: &ImageData, left: u32, top: u32) -> Result<()> {

View File

@@ -99,6 +99,7 @@ impl SoftpalScript {
} else {
let mut path = std::path::PathBuf::from(filename);
path.set_file_name(name);
path = crate::utils::files::get_ignorecase_path(&path)?;
std::fs::read(path).map_err(|e| anyhow::anyhow!("Failed to read file {}: {}", name, e))
}
}

View File

@@ -1,8 +1,12 @@
//! Utilities for File Operations
use crate::scripts::{ALL_EXTS, ARCHIVE_EXTS};
#[cfg(not(windows))]
use std::ffi::OsString;
use std::fs;
use std::io;
use std::io::{Read, Write};
#[cfg(not(windows))]
use std::path::Component;
use std::path::{Path, PathBuf};
/// Returns the relative path from `root` to `target`.
@@ -336,3 +340,129 @@ pub fn sanitize_path(path: &str) -> String {
result
}
pub fn get_ignorecase_path<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
#[cfg(windows)]
return Ok(path.as_ref().to_path_buf());
#[cfg(not(windows))]
{
let path = path.as_ref();
// If the path exists as is, return it
if path.exists() {
return Ok(path.to_path_buf());
}
{
// Helper: try to resolve the remaining tail components starting from base,
// performing case-insensitive matches for each step.
fn resolve_from_base(base: PathBuf, tail: &[OsString]) -> io::Result<Option<PathBuf>> {
let mut cur = base;
for comp in tail {
let direct = cur.join(comp);
if direct.exists() {
cur = direct;
continue;
}
if !cur.is_dir() {
return Ok(None);
}
let mut found = None;
for entry in fs::read_dir(&cur)? {
let entry = entry?;
let name = entry.file_name();
if name
.to_string_lossy()
.eq_ignore_ascii_case(&comp.to_string_lossy())
{
found = Some(cur.join(name));
break;
}
}
match found {
Some(p) => cur = p,
None => return Ok(None),
}
}
Ok(Some(cur))
}
let orig = path;
// If it exists as-is, return immediately
if orig.exists() {
return Ok(orig.to_path_buf());
}
// Collect components as OsString (preserve Prefix/RootDir as components)
let comps: Vec<OsString> = orig
.components()
.map(|c| match c {
Component::Prefix(p) => p.as_os_str().to_os_string(),
Component::RootDir => OsString::from(std::path::MAIN_SEPARATOR.to_string()),
other => other.as_os_str().to_os_string(),
})
.collect();
// Try replacing components from the bottom (leaf) upward.
let len = comps.len();
for idx in (0..len).rev() {
// Build parent path from comps[0..idx]
let mut parent = PathBuf::new();
for j in 0..idx {
parent.push(&comps[j]);
}
if parent.as_os_str().is_empty() {
parent = PathBuf::from(".");
}
// If parent doesn't exist or is not a directory, skip this level
if !parent.exists() || !parent.is_dir() {
continue;
}
// Look for a case-insensitive match for the component at idx inside parent
let target = &comps[idx];
let mut matched_name: Option<OsString> = None;
for entry in fs::read_dir(&parent)? {
let entry = entry?;
let name = entry.file_name();
if name
.to_string_lossy()
.eq_ignore_ascii_case(&target.to_string_lossy())
{
matched_name = Some(name);
break;
}
}
if let Some(name) = matched_name {
// Reconstruct candidate path: parent + matched_name + remaining original tail
let candidate_base = parent.join(name);
let tail: Vec<OsString> = comps.iter().skip(idx + 1).cloned().collect();
if tail.is_empty() {
if candidate_base.exists() {
return Ok(candidate_base);
} else {
// Even if leaf matched, final file may not exist (e.g., different deeper casing),
// attempt to resolve remaining components (none here) so treat as not found.
continue;
}
}
if let Some(resolved) = resolve_from_base(candidate_base, &tail)? {
return Ok(resolved);
}
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"Path {} not found (case-insensitive search failed)",
path.display()
),
))
}
}
}