use crate::{
error::Error,
strings::{NsisString, StringSegment, decode_short},
};
const NS3_LANG: u8 = 0x01;
const NS3_SHELL: u8 = 0x02;
const NS3_VAR: u8 = 0x03;
const NS3_SKIP: u8 = 0x04;
const NS2_SKIP: u8 = 0xFC;
const NS2_VAR: u8 = 0xFD;
const NS2_SHELL: u8 = 0xFE;
const NS2_LANG: u8 = 0xFF;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AnsiCode {
Literal,
Skip,
Var,
Shell,
Lang,
}
fn classify_byte(b: u8) -> AnsiCode {
match b {
NS3_LANG | NS2_LANG => AnsiCode::Lang,
NS3_SHELL | NS2_SHELL => AnsiCode::Shell,
NS3_VAR | NS2_VAR => AnsiCode::Var,
NS3_SKIP | NS2_SKIP => AnsiCode::Skip,
_ => AnsiCode::Literal,
}
}
pub fn read_ansi_string(table: &[u8], offset: usize) -> Result<NsisString, Error> {
let mut segments = Vec::new();
let mut literal = String::new();
let mut pos = offset;
while let Some(&b) = table.get(pos) {
if b == 0 {
break;
}
let code = classify_byte(b);
if code != AnsiCode::Literal {
if code == AnsiCode::Skip {
pos = pos.saturating_add(1);
if let Some(&next) = table.get(pos) {
literal.push(next as char);
}
pos = pos.saturating_add(1);
continue;
}
if !literal.is_empty() {
segments.push(StringSegment::Literal(literal.clone()));
literal.clear();
}
let (Some(p1), Some(p2)) = (pos.checked_add(1), pos.checked_add(2)) else {
break;
};
let (Some(&hi), Some(&lo)) = (table.get(p1), table.get(p2)) else {
break;
};
let val = decode_short(hi, lo);
pos = pos.saturating_add(3);
match code {
AnsiCode::Var => segments.push(StringSegment::Variable(val)),
AnsiCode::Shell => segments.push(StringSegment::ShellFolder(val)),
AnsiCode::Lang => segments.push(StringSegment::LangString(val)),
_ => {}
}
} else {
literal.push(b as char);
pos = pos.saturating_add(1);
}
}
if !literal.is_empty() {
segments.push(StringSegment::Literal(literal));
}
Ok(NsisString { segments })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::strings::encode_short;
#[test]
fn plain_string() {
let table = b"Hello World\0rest";
let s = read_ansi_string(table, 0).unwrap();
assert_eq!(s.segments.len(), 1);
assert_eq!(s.segments[0], StringSegment::Literal("Hello World".into()));
}
#[test]
fn nsis3_variable() {
let (b0, b1) = encode_short(21);
let mut table = Vec::new();
table.extend_from_slice(b"Install to ");
table.push(NS3_VAR);
table.push(b0);
table.push(b1);
table.push(0);
let s = read_ansi_string(&table, 0).unwrap();
assert_eq!(s.segments.len(), 2);
assert_eq!(s.segments[0], StringSegment::Literal("Install to ".into()));
assert_eq!(s.segments[1], StringSegment::Variable(21));
assert_eq!(s.to_string(), "Install to $INSTDIR");
}
#[test]
fn nsis2_variable() {
let (b0, b1) = encode_short(21);
let mut table = Vec::new();
table.extend_from_slice(b"Dir: ");
table.push(NS2_VAR);
table.push(b0);
table.push(b1);
table.push(0);
let s = read_ansi_string(&table, 0).unwrap();
assert_eq!(s.segments.len(), 2);
assert_eq!(s.segments[0], StringSegment::Literal("Dir: ".into()));
assert_eq!(s.segments[1], StringSegment::Variable(21));
}
#[test]
fn nsis2_shell_folder() {
let (b0, b1) = encode_short(0x001A); let mut table = Vec::new();
table.push(NS2_SHELL);
table.push(b0);
table.push(b1);
table.extend_from_slice(b"\\MyApp\0");
let s = read_ansi_string(&table, 0).unwrap();
assert_eq!(s.segments.len(), 2);
assert_eq!(s.segments[0], StringSegment::ShellFolder(0x001A));
assert_eq!(s.segments[1], StringSegment::Literal("\\MyApp".into()));
}
#[test]
fn nsis3_skip_code() {
let mut table = Vec::new();
table.extend_from_slice(b"A");
table.push(NS3_SKIP);
table.push(0x03); table.extend_from_slice(b"B\0");
let s = read_ansi_string(&table, 0).unwrap();
assert_eq!(s.segments.len(), 1);
assert_eq!(s.segments[0], StringSegment::Literal("A\x03B".into()));
}
#[test]
fn nsis2_skip_code() {
let table = vec![NS2_SKIP, NS2_VAR, 0];
let s = read_ansi_string(&table, 0).unwrap();
assert_eq!(s.segments.len(), 1);
assert_eq!(
s.segments[0],
StringSegment::Literal(String::from(NS2_VAR as char))
);
}
#[test]
fn string_at_offset() {
let table = b"\0\0\0Hello\0";
let s = read_ansi_string(table, 3).unwrap();
assert_eq!(s.segments.len(), 1);
assert_eq!(s.segments[0], StringSegment::Literal("Hello".into()));
}
#[test]
fn empty_string() {
let table = b"\0";
let s = read_ansi_string(table, 0).unwrap();
assert!(s.is_empty());
}
}