use std::fs;
use std::path::Path;
use r2smt_common::{Address, Error, Result};
use r2smt_core::{Confidence, FindingKind};
use serde::{Deserialize, Serialize};
pub const MANIFEST_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PatchRecord {
pub address: Address,
pub strategy: String,
pub kind: FindingKind,
pub confidence: Confidence,
pub original_bytes_hex: String,
pub patched_bytes_hex: String,
pub rationale: String,
}
impl PatchRecord {
pub fn original_bytes(&self) -> Result<Vec<u8>> {
hex::decode(&self.original_bytes_hex)
.map_err(|e| Error::parse("patch_record.original_bytes_hex", e.to_string()))
}
pub fn patched_bytes(&self) -> Result<Vec<u8>> {
hex::decode(&self.patched_bytes_hex)
.map_err(|e| Error::parse("patch_record.patched_bytes_hex", e.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PatchManifest {
pub manifest_version: u32,
pub r2smt_version: String,
pub binary: String,
pub binary_sha256_before: String,
pub binary_sha256_after: String,
pub backup_path: String,
pub operations: Vec<PatchRecord>,
}
impl PatchManifest {
pub const DEFAULT_FILE_NAME: &'static str = "r2smt.manifest.json";
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self)
.map_err(|e| Error::parse("patch_manifest", e.to_string()))
}
pub fn write_to(&self, path: impl AsRef<Path>) -> Result<()> {
let json = self.to_json()?;
fs::write(path, json)?;
Ok(())
}
pub fn read_from(path: impl AsRef<Path>) -> Result<Self> {
let raw = fs::read_to_string(path)?;
let parsed: Self = serde_json::from_str(&raw)
.map_err(|e| Error::parse("patch_manifest", e.to_string()))?;
if parsed.manifest_version != MANIFEST_VERSION {
return Err(Error::parse(
"patch_manifest",
format!(
"unsupported manifest version {got} (this build only handles {expected})",
got = parsed.manifest_version,
expected = MANIFEST_VERSION,
),
));
}
Ok(parsed)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use tempfile::NamedTempFile;
use super::*;
fn sample_manifest() -> PatchManifest {
PatchManifest {
manifest_version: MANIFEST_VERSION,
r2smt_version: "0.1.0".into(),
binary: "/tmp/sample.exe".into(),
binary_sha256_before: "a".repeat(64),
binary_sha256_after: "b".repeat(64),
backup_path: "/tmp/sample.exe.r2smt.bak".into(),
operations: vec![PatchRecord {
address: Address(0x40_1050),
strategy: "nop_jcc".into(),
kind: FindingKind::DeadBranch,
confidence: Confidence::High,
original_bytes_hex: "7505".into(),
patched_bytes_hex: "9090".into(),
rationale: "jne is never taken".into(),
}],
}
}
#[test]
fn manifest_round_trips_through_json() {
let original = sample_manifest();
let json = original.to_json().unwrap();
let back: PatchManifest = serde_json::from_str(&json).unwrap();
assert_eq!(back, original);
}
#[test]
fn manifest_round_trips_through_disk() {
let original = sample_manifest();
let tmp = NamedTempFile::new().unwrap();
original.write_to(tmp.path()).unwrap();
let back = PatchManifest::read_from(tmp.path()).unwrap();
assert_eq!(back, original);
}
#[test]
fn read_from_rejects_unknown_version() {
let mut bad = sample_manifest();
bad.manifest_version = MANIFEST_VERSION + 1;
let tmp = NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), bad.to_json().unwrap()).unwrap();
let err = PatchManifest::read_from(tmp.path()).unwrap_err();
let rendered = err.to_string();
assert!(rendered.contains("unsupported manifest version"));
}
#[test]
fn patch_record_decodes_hex_round_trip() {
let record = &sample_manifest().operations[0];
assert_eq!(record.original_bytes().unwrap(), vec![0x75, 0x05]);
assert_eq!(record.patched_bytes().unwrap(), vec![0x90, 0x90]);
}
}