use crate::Result;
use crate::fs::grf::crypt;
use crate::fs::grf::encoding;
pub const GRF_FLAG_FILE: u8 = 0x01;
pub const GRF_FLAG_MIXCRYPT: u8 = 0x02;
pub const GRF_FLAG_DES: u8 = 0x04;
#[derive(Debug, Clone, Copy)]
pub(crate) struct RawEntry {
pub len: u32,
pub len_aligned: u32,
pub size: u32,
pub flags: u8,
pub pos: u32,
}
impl RawEntry {
pub const SIZE: usize = 17;
fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < Self::SIZE {
return Err(crate::Error::InvalidImage(format!(
"grf: table entry needs {} bytes, got {}",
Self::SIZE,
buf.len()
)));
}
Ok(Self {
len: u32::from_le_bytes(buf[0..4].try_into().unwrap()),
len_aligned: u32::from_le_bytes(buf[4..8].try_into().unwrap()),
size: u32::from_le_bytes(buf[8..12].try_into().unwrap()),
flags: buf[12],
pos: u32::from_le_bytes(buf[13..17].try_into().unwrap()),
})
}
fn encode_into(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.len.to_le_bytes());
out.extend_from_slice(&self.len_aligned.to_le_bytes());
out.extend_from_slice(&self.size.to_le_bytes());
out.push(self.flags);
out.extend_from_slice(&self.pos.to_le_bytes());
}
}
#[derive(Debug, Clone)]
pub struct Entry {
pub name: String,
pub size: u32,
pub len: u32,
pub len_aligned: u32,
pub pos: u32,
pub flags: u8,
}
impl Entry {
pub fn crypto_cycle(&self) -> Option<i32> {
if self.flags & GRF_FLAG_MIXCRYPT != 0 {
let mut cycle = 1i32;
let mut step = 10u32;
while self.len >= step {
cycle += 1;
step = step.saturating_mul(10);
if step == u32::MAX {
break;
}
}
Some(cycle)
} else if self.flags & GRF_FLAG_DES != 0 {
Some(0)
} else {
None
}
}
}
pub(crate) fn decode_v200(buf: &[u8]) -> Result<Vec<Entry>> {
let mut out = Vec::new();
let mut pos = 0;
while pos < buf.len() {
let nul = match buf[pos..].iter().position(|&b| b == 0) {
Some(i) => i,
None => {
return Err(crate::Error::InvalidImage(
"grf: v0x200 table truncated mid-filename".into(),
));
}
};
let name_bytes = &buf[pos..pos + nul];
pos += nul + 1;
if pos + RawEntry::SIZE > buf.len() {
return Err(crate::Error::InvalidImage(
"grf: v0x200 table truncated mid-entry".into(),
));
}
let raw = RawEntry::decode(&buf[pos..pos + RawEntry::SIZE])?;
pos += RawEntry::SIZE;
if raw.flags & GRF_FLAG_FILE == 0 || raw.size == 0 {
continue;
}
let name = encoding::cp949_to_utf8(name_bytes).into_owned();
out.push(Entry {
name,
size: raw.size,
len: raw.len,
len_aligned: raw.len_aligned,
pos: raw.pos,
flags: raw.flags,
});
}
Ok(out)
}
pub(crate) fn decode_v102(buf: &[u8]) -> Result<Vec<Entry>> {
let mut out = Vec::new();
let mut pos = 0;
while pos < buf.len() {
if pos + 6 > buf.len() {
return Err(crate::Error::InvalidImage(
"grf: v0x102 table truncated at length prefix".into(),
));
}
let padded_fn_len =
u32::from_le_bytes(buf[pos..pos + 4].try_into().unwrap()).wrapping_sub(2) as usize;
pos += 6;
if pos + padded_fn_len + RawEntry::SIZE > buf.len() {
return Err(crate::Error::InvalidImage(
"grf: v0x102 entry exceeds table bounds".into(),
));
}
let mut name_bytes = buf[pos..pos + padded_fn_len].to_vec();
pos += padded_fn_len;
crypt::decode_filename(&mut name_bytes);
let real_len = name_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(name_bytes.len());
name_bytes.truncate(real_len);
let raw = RawEntry::decode(&buf[pos..pos + RawEntry::SIZE])?;
pos += RawEntry::SIZE;
if raw.flags & GRF_FLAG_FILE == 0 || raw.size == 0 {
continue;
}
let actual_len = raw.len.wrapping_sub(raw.size).wrapping_sub(715);
let actual_aligned = raw.len_aligned.wrapping_sub(37579);
let name = encoding::cp949_to_utf8(&name_bytes).into_owned();
let mut entry = Entry {
name,
size: raw.size,
len: actual_len,
len_aligned: actual_aligned,
pos: raw.pos,
flags: raw.flags,
};
apply_v102_crypto_heuristic(&mut entry);
out.push(entry);
}
Ok(out)
}
fn apply_v102_crypto_heuristic(entry: &mut Entry) {
if entry.flags & (GRF_FLAG_MIXCRYPT | GRF_FLAG_DES) != 0 {
return;
}
const NO_MIX: &[&[u8; 3]] = &[b"gnd", b"gat", b"act", b"str"];
let lower = entry.name.to_ascii_lowercase();
let is_no_mix = lower.len() >= 4
&& lower.as_bytes()[lower.len() - 4] == b'.'
&& NO_MIX
.iter()
.any(|ext| &lower.as_bytes()[lower.len() - 3..] == ext.as_slice());
if is_no_mix {
entry.flags |= GRF_FLAG_DES;
} else {
entry.flags |= GRF_FLAG_MIXCRYPT;
}
}
pub(crate) fn encode_v200(entries: &[Entry]) -> Vec<u8> {
let mut out = Vec::new();
for e in entries {
let cp949 = encoding::utf8_to_cp949(&e.name);
out.extend_from_slice(&cp949);
out.push(0); let raw = RawEntry {
len: e.len,
len_aligned: e.len_aligned,
size: e.size,
flags: e.flags,
pos: e.pos,
};
raw.encode_into(&mut out);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v200_round_trip() {
let entries = vec![
Entry {
name: "data/info.txt".into(),
size: 100,
len: 50,
len_aligned: 52,
pos: 0,
flags: GRF_FLAG_FILE,
},
Entry {
name: "한글파일.txt".into(),
size: 200,
len: 80,
len_aligned: 80,
pos: 52,
flags: GRF_FLAG_FILE,
},
];
let blob = encode_v200(&entries);
let back = decode_v200(&blob).unwrap();
assert_eq!(back.len(), 2);
assert_eq!(back[0].name, "data/info.txt");
assert_eq!(back[1].name, "한글파일.txt");
assert_eq!(back[1].len_aligned, 80);
}
#[test]
fn v200_skips_directory_markers() {
let mut blob = Vec::new();
blob.extend_from_slice(b"some/dir\0");
RawEntry {
len: 0,
len_aligned: 0,
size: 0,
flags: 0,
pos: 0,
}
.encode_into(&mut blob);
blob.extend_from_slice(b"real.txt\0");
RawEntry {
len: 5,
len_aligned: 8,
size: 10,
flags: GRF_FLAG_FILE,
pos: 0,
}
.encode_into(&mut blob);
let entries = decode_v200(&blob).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "real.txt");
}
#[test]
fn crypto_cycle() {
let e = Entry {
name: "x".into(),
size: 0,
len: 9999,
len_aligned: 0,
pos: 0,
flags: GRF_FLAG_FILE | GRF_FLAG_MIXCRYPT,
};
assert_eq!(e.crypto_cycle(), Some(4));
let e2 = Entry {
name: "x".into(),
size: 0,
len: 100,
len_aligned: 0,
pos: 0,
flags: GRF_FLAG_FILE | GRF_FLAG_DES,
};
assert_eq!(e2.crypto_cycle(), Some(0));
let e3 = Entry {
name: "x".into(),
size: 0,
len: 100,
len_aligned: 0,
pos: 0,
flags: GRF_FLAG_FILE,
};
assert_eq!(e3.crypto_cycle(), None);
}
#[test]
fn v102_heuristic_picks_des_for_gnd() {
let mut e = Entry {
name: "data/map.gnd".into(),
size: 100,
len: 50,
len_aligned: 52,
pos: 0,
flags: GRF_FLAG_FILE,
};
apply_v102_crypto_heuristic(&mut e);
assert_eq!(e.flags & GRF_FLAG_DES, GRF_FLAG_DES);
assert_eq!(e.flags & GRF_FLAG_MIXCRYPT, 0);
}
#[test]
fn v102_heuristic_picks_mixcrypt_for_others() {
let mut e = Entry {
name: "data/info.txt".into(),
size: 100,
len: 50,
len_aligned: 52,
pos: 0,
flags: GRF_FLAG_FILE,
};
apply_v102_crypto_heuristic(&mut e);
assert_eq!(e.flags & GRF_FLAG_MIXCRYPT, GRF_FLAG_MIXCRYPT);
assert_eq!(e.flags & GRF_FLAG_DES, 0);
}
}