use ssh_stamp_ota::{OtaHeader, tlv};
use clap::{ArgAction, Command};
use sha2::{Digest, Sha256};
use std::{
io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
};
const OTA_PACKER_VERSION: &str = env!("CARGO_PKG_VERSION");
const OK: i32 = 0;
const USAGE: i32 = 1;
const FILE_NOT_FOUND: i32 = 2;
const NOT_A_FILE: i32 = 3;
const OPEN_FAILED: i32 = 4;
const READ_FAILED: i32 = 5;
const CREATE_FAILED: i32 = 6;
const WRITE_FAILED: i32 = 7;
const SEEK_FAILED: i32 = 8;
const CHECKSUM_MISMATCH: i32 = 9;
const FILE_TOO_LARGE: i32 = 10;
fn main() {
let matches = Command::new("packer")
.about(format!("SSH-Stamp utility {OTA_PACKER_VERSION} to pack (unpack) OTA update files adding the required metadata."))
.arg(clap::arg!(<FILE> "The file to process").required(true))
.arg(
clap::arg!(-u --unpack "Unpacks a OTA file. Will save to <file> with .ota.npkd extension")
.action(ArgAction::SetTrue)
.conflicts_with("pack"),
)
.arg(
clap::arg!(-p --pack "(default) Packs a binary file as an OTA file. Will save to <file>.ota")
.action(ArgAction::SetTrue)
.conflicts_with("unpack"),
)
.get_matches();
let Some(file_path) = matches.get_one::<String>("FILE") else {
eprintln!("Error: No file provided");
std::process::exit(USAGE);
};
let file_path = PathBuf::from(file_path);
if !file_path.exists() {
eprintln!("Error: File '{}' does not exist", file_path.display());
std::process::exit(FILE_NOT_FOUND);
}
if !file_path.is_file() {
eprintln!(
"Error: File '{}' is not a regular file",
file_path.display()
);
std::process::exit(NOT_A_FILE);
}
if matches.get_flag("unpack") {
std::process::exit(unpack_ota(&file_path));
}
std::process::exit(pack_bin(&file_path));
}
fn unpack_ota(file_path: &Path) -> i32 {
println!("Unpacking BIN from OTA file {}...", file_path.display());
let Ok(file) = std::fs::File::open(file_path) else {
eprintln!("Error: Could not open file '{}'", file_path.display());
return OPEN_FAILED;
};
let mut reader = std::io::BufReader::new(file);
let mut buffer = [0u8; 512];
let Ok(_) = reader.read(&mut buffer) else {
eprintln!("Error: Could not read from file '{}'", file_path.display());
return READ_FAILED;
};
let Ok((header, seek_to_bin)) = OtaHeader::deserialize(&buffer) else {
eprintln!(
"Error: Could not parse OTA header from file '{}'",
file_path.display(),
);
return READ_FAILED;
};
println!("Found OTA header: {header:?}");
let mut file_path_bin = file_path.to_path_buf();
file_path_bin.set_extension("ota.npkd");
println!("Saving unpacked BIN file to: {}", file_path_bin.display());
let Ok(mut bin_file) = std::fs::File::create(&file_path_bin) else {
eprintln!(
"Error: Could not create BIN file '{}'",
file_path_bin.display(),
);
return CREATE_FAILED;
};
if let Err(e) = reader.seek(SeekFrom::Start(seek_to_bin as u64)) {
eprintln!("Error: Could not seek to binary data: {e}");
return SEEK_FAILED;
}
let mut recover_ota_bin_hasher = Sha256::new();
let mut r: usize;
while {
r = reader.read(&mut buffer).unwrap_or(0);
r
} > 0
{
let Ok(_) = bin_file.write(&buffer[..r]) else {
eprintln!(
"Error: Could not write to BIN file '{}'",
file_path_bin.display(),
);
return WRITE_FAILED;
};
recover_ota_bin_hasher.update(&buffer[..r]);
}
let recovered_firmware_sha256 = recover_ota_bin_hasher.finalize();
let Some(expected_sha256) = header.sha256_checksum else {
eprintln!("Error: OTA header has no SHA-256 checksum to verify against");
return CHECKSUM_MISMATCH;
};
if recovered_firmware_sha256.as_slice() != expected_sha256 {
eprintln!(
"Error: Recovered firmware SHA-256 does not match expected value!\nExpected: {expected_sha256:x?}\nRecovered: {recovered_firmware_sha256:x?}"
);
return CHECKSUM_MISMATCH;
}
println!("Recovered firmware SHA-256 matches expected value.");
OK
}
fn pack_bin(file_path: &Path) -> i32 {
println!("Packing {} as OTA...", file_path.display());
let metadata = match file_path.metadata() {
Ok(metadata) => metadata,
Err(e) => {
eprintln!(
"Error: Could not retrieve metadata for file '{}': {e}",
file_path.display()
);
return OPEN_FAILED;
}
};
let Ok(firmware_size) = u32::try_from(metadata.len()) else {
eprintln!(
"Error: File '{}' is too large (max 4GB supported)",
file_path.display()
);
return FILE_TOO_LARGE;
};
println!("Bin file size: {firmware_size} bytes");
let mut hasher = Sha256::new();
let Ok(read) = std::fs::read(file_path) else {
eprintln!("Error: Could not read file '{}'", file_path.display());
return READ_FAILED;
};
hasher.update(&read);
let firmware_sha256 = hasher.finalize();
println!("Firmware SHA-256: {firmware_sha256:x}");
let ota_type = tlv::OTA_TYPE_VALUE_SSH_STAMP;
println!("OTA Type Number: {ota_type} (SSH-Stamp)");
let mut ota_file_path = file_path.to_path_buf();
ota_file_path.set_extension("ota");
println!("Saving OTA file to: {}", ota_file_path.display());
let Ok(mut ota_file) = std::fs::File::create(&ota_file_path) else {
eprintln!(
"Error: Could not create OTA file '{}'",
ota_file_path.display(),
);
return CREATE_FAILED;
};
let mut buf = [0u8; 512];
let header_len =
OtaHeader::new(ota_type, firmware_sha256.as_slice(), firmware_size).serialize(&mut buf);
println!("OTA header length: {header_len} bytes");
let Ok(bytes) = ota_file.write(&buf[..header_len]) else {
eprintln!(
"Error: Could not write to OTA file '{}'",
ota_file_path.display(),
);
return WRITE_FAILED;
};
println!("Wrote {bytes} bytes of OTA header");
let Ok(bytes) = ota_file.write(&read) else {
eprintln!(
"Error: Could not write firmware data to OTA file '{}'",
ota_file_path.display(),
);
return WRITE_FAILED;
};
println!("Wrote {bytes} bytes of firmware data");
OK
}