pub mod ansi;
pub mod park;
pub mod unicode;
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StringEncoding {
Ansi,
Unicode,
Park,
}
impl fmt::Display for StringEncoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
StringEncoding::Ansi => "ANSI",
StringEncoding::Unicode => "Unicode",
StringEncoding::Park => "Park",
};
f.write_str(s)
}
}
pub fn detect_encoding(string_table: &[u8]) -> StringEncoding {
if string_table.len() < 4 {
return StringEncoding::Ansi;
}
if string_table.first().copied() != Some(0) || string_table.get(1).copied() != Some(0) {
return StringEncoding::Ansi;
}
let limit = string_table.len().min(4096) & !1;
for i in (2..limit).step_by(2) {
let Some(pair) = string_table.get(i..).and_then(|s| s.first_chunk::<2>()) else {
break;
};
let ch = u16::from_le_bytes(*pair);
if ch == 0 {
continue;
}
if (0x0001..=0x0004).contains(&ch) {
return StringEncoding::Unicode;
}
if (0xE000..=0xE003).contains(&ch) {
return StringEncoding::Park;
}
}
StringEncoding::Unicode
}
#[inline]
pub fn decode_short(b0: u8, b1: u8) -> u16 {
(((b1 & 0x7F) as u16) << 7) | ((b0 & 0x7F) as u16)
}
#[inline]
pub fn encode_short(value: u16) -> (u8, u8) {
let b0 = ((value & 0x7F) | 0x80) as u8;
let b1 = (((value >> 7) & 0x7F) | 0x80) as u8;
(b0, b1)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StringSegment {
Literal(String),
Variable(u16),
ShellFolder(u16),
LangString(u16),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NsisString {
pub segments: Vec<StringSegment>,
}
impl NsisString {
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
pub fn to_path(&self) -> String {
let mut result = String::new();
for seg in &self.segments {
match seg {
StringSegment::Literal(s) => result.push_str(s),
StringSegment::Variable(idx) => {
match *idx {
21 | 22 => {}
25 => result.push_str("_temp"),
26 => result.push_str("_plugins"),
23 => result.push_str("_exedir"),
_ => {
let name = variable_name(*idx);
let stripped = name.strip_prefix('$').unwrap_or(&name);
result.push('_');
result.push_str(stripped);
}
}
}
StringSegment::ShellFolder(raw) => {
let name = shell_folder_name(*raw);
let stripped = name.strip_prefix('$').unwrap_or(&name);
result.push('_');
result.push_str(stripped);
}
StringSegment::LangString(_) => {
}
}
}
result
.replace('\\', "/")
.replace("//", "/")
.replace("..", "_")
.trim_start_matches('/')
.to_string()
}
}
impl fmt::Display for NsisString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for seg in &self.segments {
match seg {
StringSegment::Literal(s) => write!(f, "{s}")?,
StringSegment::Variable(idx) => {
write!(f, "{}", variable_name(*idx))?;
}
StringSegment::ShellFolder(raw) => {
write!(f, "{}", shell_folder_name(*raw))?;
}
StringSegment::LangString(idx) => {
write!(f, "${{LANG:{idx}}}")?;
}
}
}
Ok(())
}
}
pub fn read_nsis_string(
table: &[u8],
offset: usize,
encoding: StringEncoding,
) -> Result<NsisString, crate::error::Error> {
if offset >= table.len() {
return Err(crate::error::Error::InvalidStringOffset {
offset: offset as u32,
});
}
match encoding {
StringEncoding::Ansi => ansi::read_ansi_string(table, offset),
StringEncoding::Unicode => unicode::read_unicode_string(table, offset),
StringEncoding::Park => park::read_park_string(table, offset),
}
}
const NUM_INTERNAL_VARS: u16 = 32;
static VARIABLE_NAMES: [&str; 32] = [
"$0",
"$1",
"$2",
"$3",
"$4",
"$5",
"$6",
"$7",
"$8",
"$9", "$R0",
"$R1",
"$R2",
"$R3",
"$R4",
"$R5",
"$R6",
"$R7",
"$R8",
"$R9", "$CMDLINE", "$INSTDIR", "$OUTDIR", "$EXEDIR", "$LANGUAGE", "$TEMP", "$PLUGINSDIR", "$EXEPATH", "$EXEFILE", "$HWNDPARENT", "$_CLICK", "$_OUTDIR", ];
pub fn variable_name(index: u16) -> std::borrow::Cow<'static, str> {
if let Some(name) = VARIABLE_NAMES.get(index as usize) {
std::borrow::Cow::Borrowed(name)
} else {
std::borrow::Cow::Owned(format!("$_{}_", index.saturating_sub(NUM_INTERNAL_VARS)))
}
}
static SHELL_FOLDER_NAMES: &[Option<&str>] = &[
Some("DESKTOP"), Some("INTERNET"), Some("SMPROGRAMS"), Some("CONTROLS"), Some("PRINTERS"), Some("DOCUMENTS"), Some("FAVORITES"), Some("SMSTARTUP"), Some("RECENT"), Some("SENDTO"), Some("BITBUCKET"), Some("STARTMENU"), None, Some("MUSIC"), Some("VIDEOS"), None, Some("DESKTOP"), Some("DRIVES"), Some("NETWORK"), Some("NETHOOD"), Some("FONTS"), Some("TEMPLATES"), Some("STARTMENU"), Some("SMPROGRAMS"), Some("SMSTARTUP"), Some("DESKTOP"), Some("APPDATA"), Some("PRINTHOOD"), Some("LOCALAPPDATA"), Some("ALTSTARTUP"), Some("ALTSTARTUP"), Some("FAVORITES"), Some("INTERNET_CACHE"), Some("COOKIES"), Some("HISTORY"), Some("APPDATA"), Some("WINDIR"), Some("SYSDIR"), Some("PROGRAMFILES"), Some("PICTURES"), Some("PROFILE"), Some("SYSTEMX86"), Some("PROGRAMFILESX86"), Some("PROGRAMFILES_COMMON"), Some("PROGRAMFILES_COMMONX86"), Some("TEMPLATES"), Some("DOCUMENTS"), Some("ADMINTOOLS"), Some("ADMINTOOLS"), Some("CONNECTIONS"), None, None, None, Some("MUSIC"), Some("PICTURES"), Some("VIDEOS"), Some("RESOURCES"), Some("RESOURCES_LOCALIZED"), Some("COMMON_OEM_LINKS"), Some("CDBURN_AREA"), None, Some("COMPUTERSNEARME"), ];
pub fn shell_folder_name(raw: u16) -> String {
let index1 = (raw & 0xFF) as usize;
let index2 = (raw >> 8) as usize;
if index1 & 0x80 != 0 {
let is_64 = index1 & 0x40 != 0;
let suffix = if is_64 { "64" } else { "" };
return format!("$PROGRAMFILES{suffix}");
}
if let Some(Some(name)) = SHELL_FOLDER_NAMES.get(index1) {
return format!("${name}");
}
if let Some(Some(name)) = SHELL_FOLDER_NAMES.get(index2) {
return format!("${name}");
}
format!("$SHELL({index1},{index2})")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_encoding_unicode() {
assert_eq!(
detect_encoding(&[0x00, 0x00, 0x48, 0x00]),
StringEncoding::Unicode
);
assert_eq!(
detect_encoding(&[0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x42, 0x00]),
StringEncoding::Unicode
);
}
#[test]
fn detect_encoding_park() {
assert_eq!(
detect_encoding(&[0x00, 0x00, 0x01, 0xE0, 0x15, 0x00]),
StringEncoding::Park
);
assert_eq!(
detect_encoding(&[0x00, 0x00, 0x02, 0xE0, 0x1A, 0x00]),
StringEncoding::Park
);
}
#[test]
fn detect_encoding_ansi() {
assert_eq!(
detect_encoding(&[0x00, 0x50, 0x72, 0x6F]),
StringEncoding::Ansi
);
assert_eq!(
detect_encoding(&[0x41, 0x42, 0x43, 0x00]),
StringEncoding::Ansi
);
assert_eq!(
detect_encoding(&[0x00, 0xFE, 0x1A, 0x23]),
StringEncoding::Ansi
);
}
#[test]
fn detect_encoding_empty_or_short() {
assert_eq!(detect_encoding(&[]), StringEncoding::Ansi);
assert_eq!(detect_encoding(&[0x00]), StringEncoding::Ansi);
assert_eq!(detect_encoding(&[0x00, 0x00]), StringEncoding::Ansi);
}
#[test]
fn decode_short_values() {
assert_eq!(decode_short(0x80, 0x80), 0);
assert_eq!(decode_short(0x81, 0x80), 1);
let (b0, b1) = encode_short(0x3FFF);
assert_eq!(decode_short(b0, b1), 0x3FFF);
}
#[test]
fn encode_decode_roundtrip() {
for val in [0u16, 1, 127, 128, 255, 1000, 0x3FFF] {
let (b0, b1) = encode_short(val);
assert_eq!(decode_short(b0, b1), val, "roundtrip failed for {val}");
assert!(b0 & 0x80 != 0);
assert!(b1 & 0x80 != 0);
}
}
#[test]
fn variable_names() {
assert_eq!(variable_name(0), "$0");
assert_eq!(variable_name(9), "$9");
assert_eq!(variable_name(10), "$R0");
assert_eq!(variable_name(19), "$R9");
assert_eq!(variable_name(21), "$INSTDIR");
assert_eq!(variable_name(25), "$TEMP");
assert_eq!(variable_name(26), "$PLUGINSDIR");
assert_eq!(variable_name(30), "$_CLICK");
assert_eq!(variable_name(31), "$_OUTDIR");
assert_eq!(variable_name(32).as_ref(), "$_0_");
assert_eq!(variable_name(33).as_ref(), "$_1_");
}
#[test]
fn nsis_string_display() {
let s = NsisString {
segments: vec![
StringSegment::Variable(21),
StringSegment::Literal("\\program.exe".into()),
],
};
assert_eq!(s.to_string(), "$INSTDIR\\program.exe");
}
#[test]
fn nsis_string_display_complex() {
let s = NsisString {
segments: vec![
StringSegment::LangString(5),
StringSegment::Literal(" in ".into()),
StringSegment::ShellFolder(0x001A),
],
};
assert_eq!(s.to_string(), "${LANG:5} in $APPDATA");
}
#[test]
fn read_string_out_of_bounds() {
let table = b"hello\0";
let result = read_nsis_string(table, 100, StringEncoding::Ansi);
assert!(result.is_err());
}
#[test]
fn to_path_instdir() {
let s = NsisString {
segments: vec![
StringSegment::Variable(21), StringSegment::Literal("\\program.exe".into()),
],
};
assert_eq!(s.to_path(), "program.exe");
}
#[test]
fn to_path_pluginsdir() {
let s = NsisString {
segments: vec![
StringSegment::Variable(26), StringSegment::Literal("\\System.dll".into()),
],
};
assert_eq!(s.to_path(), "_plugins/System.dll");
}
#[test]
fn to_path_temp() {
let s = NsisString {
segments: vec![
StringSegment::Variable(25), StringSegment::Literal("\\payload.bin".into()),
],
};
assert_eq!(s.to_path(), "_temp/payload.bin");
}
#[test]
fn to_path_nested() {
let s = NsisString {
segments: vec![
StringSegment::Variable(21), StringSegment::Literal("\\Lang\\en_US.ini".into()),
],
};
assert_eq!(s.to_path(), "Lang/en_US.ini");
}
#[test]
fn to_path_shell_folder() {
let s = NsisString {
segments: vec![
StringSegment::ShellFolder(0x1A),
StringSegment::Literal("\\MyApp\\config.ini".into()),
],
};
assert_eq!(s.to_path(), "_APPDATA/MyApp/config.ini");
}
#[test]
fn to_path_no_variable() {
let s = NsisString {
segments: vec![StringSegment::Literal("readme.txt".into())],
};
assert_eq!(s.to_path(), "readme.txt");
}
}