pub mod config;
pub mod stub;
pub use config::SfxConfig;
pub use stub::{SfxFormat, SfxStub};
use std::io::Write;
use crate::{Error, Result};
#[must_use = "SFX result should be checked to verify creation completed successfully"]
#[derive(Debug, Clone)]
pub struct SfxResult {
pub total_size: u64,
pub stub_size: u64,
pub config_size: u64,
pub archive_size: u64,
}
impl SfxResult {
pub fn overhead_percent(&self) -> f64 {
if self.total_size == 0 {
0.0
} else {
((self.stub_size + self.config_size) as f64 / self.total_size as f64) * 100.0
}
}
}
#[derive(Debug, Default)]
pub struct SfxBuilder {
stub: Option<SfxStub>,
config: SfxConfig,
validate_stub: bool,
}
impl SfxBuilder {
pub fn new() -> Self {
Self {
stub: None,
config: SfxConfig::default(),
validate_stub: true,
}
}
pub fn stub(mut self, stub: SfxStub) -> Self {
self.stub = Some(stub);
self
}
pub fn config(mut self, config: SfxConfig) -> Self {
self.config = config;
self
}
pub fn validate_stub(mut self, validate: bool) -> Self {
self.validate_stub = validate;
self
}
pub fn build<W: Write>(self, output: &mut W, archive_data: &[u8]) -> Result<SfxResult> {
let stub = self
.stub
.ok_or_else(|| Error::InvalidFormat("no SFX stub provided".into()))?;
if self.validate_stub {
stub.validate()?;
}
let config_data = self.config.encode();
let stub_size = stub.write_to(output)?;
if !config_data.is_empty() {
output.write_all(&config_data).map_err(Error::Io)?;
}
output.write_all(archive_data).map_err(Error::Io)?;
let config_size = config_data.len() as u64;
let archive_size = archive_data.len() as u64;
let total_size = stub_size + config_size + archive_size;
Ok(SfxResult {
total_size,
stub_size,
config_size,
archive_size,
})
}
pub fn build_to_path(
self,
path: impl AsRef<std::path::Path>,
archive_data: &[u8],
) -> Result<SfxResult> {
let mut file = std::fs::File::create(path).map_err(Error::Io)?;
let result = self.build(&mut file, archive_data)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = file.metadata().map_err(Error::Io)?.permissions();
perms.set_mode(perms.mode() | 0o111); file.set_permissions(perms).map_err(Error::Io)?;
}
Ok(result)
}
}
pub fn create_sfx<W: Write>(
output: &mut W,
stub: &[u8],
config: Option<&SfxConfig>,
archive: &[u8],
) -> Result<u64> {
output.write_all(stub).map_err(Error::Io)?;
let mut total = stub.len() as u64;
if let Some(cfg) = config {
let config_data = cfg.encode();
if !config_data.is_empty() {
output.write_all(&config_data).map_err(Error::Io)?;
total += config_data.len() as u64;
}
}
output.write_all(archive).map_err(Error::Io)?;
total += archive.len() as u64;
Ok(total)
}
pub fn extract_archive_from_sfx(sfx_data: &[u8]) -> Result<(Vec<u8>, SfxInfo)> {
use std::io::Cursor;
let mut cursor = Cursor::new(sfx_data);
let sfx_info = crate::format::header::detect_sfx(&mut cursor)?;
let archive_offset = match sfx_info {
Some(info) => info.archive_offset,
None => 0, };
if archive_offset as usize >= sfx_data.len() {
return Err(Error::InvalidFormat(
"archive offset beyond file end".into(),
));
}
let archive_data = sfx_data[archive_offset as usize..].to_vec();
Ok((
archive_data,
SfxInfo {
archive_offset,
stub_size: archive_offset,
format: SfxFormat::detect(sfx_data),
},
))
}
#[derive(Debug, Clone)]
pub struct SfxInfo {
pub archive_offset: u64,
pub stub_size: u64,
pub format: Option<SfxFormat>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn create_minimal_archive() -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]);
data.extend_from_slice(&[0x00, 0x04]);
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
data
}
fn create_fake_pe_stub() -> Vec<u8> {
let mut stub = vec![0u8; 256];
stub[0] = b'M';
stub[1] = b'Z';
stub[0x3C] = 64;
stub[64] = b'P';
stub[65] = b'E';
stub[66] = 0;
stub[67] = 0;
stub
}
fn create_fake_elf_stub() -> Vec<u8> {
let mut stub = vec![0u8; 64];
stub[0..4].copy_from_slice(b"\x7FELF");
stub[4] = 2; stub[5] = 1; stub[16] = 2; stub[17] = 0;
stub
}
#[test]
fn test_sfx_builder_no_stub() {
let builder = SfxBuilder::new();
let archive = create_minimal_archive();
let mut output = Vec::new();
assert!(builder.build(&mut output, &archive).is_err());
}
#[test]
fn test_sfx_builder_with_stub() {
let stub_data = create_fake_pe_stub();
let stub = SfxStub::with_format(stub_data.clone(), SfxFormat::WindowsPe);
let archive = create_minimal_archive();
let mut output = Vec::new();
let result = SfxBuilder::new()
.stub(stub)
.validate_stub(false) .build(&mut output, &archive)
.unwrap();
assert_eq!(result.stub_size, stub_data.len() as u64);
assert_eq!(result.archive_size, archive.len() as u64);
assert_eq!(result.config_size, 0);
assert_eq!(
result.total_size,
stub_data.len() as u64 + archive.len() as u64
);
}
#[test]
fn test_sfx_builder_with_config() {
let stub_data = create_fake_pe_stub();
let stub = SfxStub::with_format(stub_data, SfxFormat::WindowsPe);
let config = SfxConfig::new().title("Test Installer").progress(true);
let archive = create_minimal_archive();
let mut output = Vec::new();
let result = SfxBuilder::new()
.stub(stub)
.config(config)
.validate_stub(false)
.build(&mut output, &archive)
.unwrap();
assert!(result.config_size > 0);
assert!(result.total_size > result.stub_size + result.archive_size);
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains(";!@Install@!UTF-8!"));
assert!(output_str.contains("Title=\"Test Installer\""));
}
#[test]
fn test_sfx_result_overhead() {
let result = SfxResult {
total_size: 1000,
stub_size: 100,
config_size: 50,
archive_size: 850,
};
assert!((result.overhead_percent() - 15.0).abs() < 0.001);
}
#[test]
fn test_create_sfx_function() {
let stub = create_fake_elf_stub();
let archive = create_minimal_archive();
let config = SfxConfig::new().title("Test");
let mut output = Vec::new();
let total = create_sfx(&mut output, &stub, Some(&config), &archive).unwrap();
assert!(total > (stub.len() + archive.len()) as u64); assert_eq!(output.len(), total as usize);
}
#[test]
fn test_create_sfx_no_config() {
let stub = vec![1, 2, 3, 4, 5];
let archive = create_minimal_archive();
let mut output = Vec::new();
let total = create_sfx(&mut output, &stub, None, &archive).unwrap();
assert_eq!(total, (stub.len() + archive.len()) as u64);
}
#[test]
fn test_extract_archive_from_sfx() {
let stub = create_fake_pe_stub();
let archive = create_minimal_archive();
let mut sfx_data = Vec::new();
create_sfx(&mut sfx_data, &stub, None, &archive).unwrap();
let (extracted, info) = extract_archive_from_sfx(&sfx_data).unwrap();
assert_eq!(info.archive_offset, stub.len() as u64);
assert_eq!(extracted, archive);
assert_eq!(info.format, Some(SfxFormat::WindowsPe));
}
#[test]
fn test_sfx_roundtrip_pe() {
let stub_data = create_fake_pe_stub();
let stub = SfxStub::with_format(stub_data.clone(), SfxFormat::WindowsPe);
let archive = create_minimal_archive();
let mut sfx_data = Vec::new();
let _result = SfxBuilder::new()
.stub(stub)
.validate_stub(false)
.build(&mut sfx_data, &archive)
.unwrap();
let mut cursor = Cursor::new(&sfx_data);
let detected = crate::format::header::detect_sfx(&mut cursor).unwrap();
assert!(detected.is_some());
let info = detected.unwrap();
assert_eq!(info.archive_offset, stub_data.len() as u64);
}
#[test]
fn test_sfx_roundtrip_elf() {
let stub_data = create_fake_elf_stub();
let stub = SfxStub::with_format(stub_data.clone(), SfxFormat::LinuxElf);
let archive = create_minimal_archive();
let mut sfx_data = Vec::new();
let _result = SfxBuilder::new()
.stub(stub)
.validate_stub(false)
.build(&mut sfx_data, &archive)
.unwrap();
let mut cursor = Cursor::new(&sfx_data);
let detected = crate::format::header::detect_sfx(&mut cursor).unwrap();
assert!(detected.is_some());
assert_eq!(detected.unwrap().archive_offset, stub_data.len() as u64);
}
}