1use std::fs;
7use std::path::Path;
8
9use r2smt_common::{Address, Error, Result};
10use r2smt_core::{Confidence, FindingKind};
11use serde::{Deserialize, Serialize};
12
13pub const MANIFEST_VERSION: u32 = 1;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct PatchRecord {
23 pub address: Address,
25 pub strategy: String,
27 pub kind: FindingKind,
29 pub confidence: Confidence,
31 pub original_bytes_hex: String,
34 pub patched_bytes_hex: String,
36 pub rationale: String,
38}
39
40impl PatchRecord {
41 pub fn original_bytes(&self) -> Result<Vec<u8>> {
47 hex::decode(&self.original_bytes_hex)
48 .map_err(|e| Error::parse("patch_record.original_bytes_hex", e.to_string()))
49 }
50
51 pub fn patched_bytes(&self) -> Result<Vec<u8>> {
57 hex::decode(&self.patched_bytes_hex)
58 .map_err(|e| Error::parse("patch_record.patched_bytes_hex", e.to_string()))
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct PatchManifest {
65 pub manifest_version: u32,
67 pub r2smt_version: String,
69 pub binary: String,
71 pub binary_sha256_before: String,
73 pub binary_sha256_after: String,
75 pub backup_path: String,
77 pub operations: Vec<PatchRecord>,
79}
80
81impl PatchManifest {
82 pub const DEFAULT_FILE_NAME: &'static str = "r2smt.manifest.json";
85
86 pub fn to_json(&self) -> Result<String> {
92 serde_json::to_string_pretty(self)
93 .map_err(|e| Error::parse("patch_manifest", e.to_string()))
94 }
95
96 pub fn write_to(&self, path: impl AsRef<Path>) -> Result<()> {
102 let json = self.to_json()?;
103 fs::write(path, json)?;
104 Ok(())
105 }
106
107 pub fn read_from(path: impl AsRef<Path>) -> Result<Self> {
117 let raw = fs::read_to_string(path)?;
118 let parsed: Self = serde_json::from_str(&raw)
119 .map_err(|e| Error::parse("patch_manifest", e.to_string()))?;
120 if parsed.manifest_version != MANIFEST_VERSION {
121 return Err(Error::parse(
122 "patch_manifest",
123 format!(
124 "unsupported manifest version {got} (this build only handles {expected})",
125 got = parsed.manifest_version,
126 expected = MANIFEST_VERSION,
127 ),
128 ));
129 }
130 Ok(parsed)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 #![allow(clippy::unwrap_used)]
137
138 use tempfile::NamedTempFile;
139
140 use super::*;
141
142 fn sample_manifest() -> PatchManifest {
143 PatchManifest {
144 manifest_version: MANIFEST_VERSION,
145 r2smt_version: "0.1.0".into(),
146 binary: "/tmp/sample.exe".into(),
147 binary_sha256_before: "a".repeat(64),
148 binary_sha256_after: "b".repeat(64),
149 backup_path: "/tmp/sample.exe.r2smt.bak".into(),
150 operations: vec![PatchRecord {
151 address: Address(0x40_1050),
152 strategy: "nop_jcc".into(),
153 kind: FindingKind::DeadBranch,
154 confidence: Confidence::High,
155 original_bytes_hex: "7505".into(),
156 patched_bytes_hex: "9090".into(),
157 rationale: "jne is never taken".into(),
158 }],
159 }
160 }
161
162 #[test]
163 fn manifest_round_trips_through_json() {
164 let original = sample_manifest();
165 let json = original.to_json().unwrap();
166 let back: PatchManifest = serde_json::from_str(&json).unwrap();
167 assert_eq!(back, original);
168 }
169
170 #[test]
171 fn manifest_round_trips_through_disk() {
172 let original = sample_manifest();
173 let tmp = NamedTempFile::new().unwrap();
174 original.write_to(tmp.path()).unwrap();
175 let back = PatchManifest::read_from(tmp.path()).unwrap();
176 assert_eq!(back, original);
177 }
178
179 #[test]
180 fn read_from_rejects_unknown_version() {
181 let mut bad = sample_manifest();
182 bad.manifest_version = MANIFEST_VERSION + 1;
183 let tmp = NamedTempFile::new().unwrap();
184 std::fs::write(tmp.path(), bad.to_json().unwrap()).unwrap();
185 let err = PatchManifest::read_from(tmp.path()).unwrap_err();
186 let rendered = err.to_string();
187 assert!(rendered.contains("unsupported manifest version"));
188 }
189
190 #[test]
191 fn patch_record_decodes_hex_round_trip() {
192 let record = &sample_manifest().operations[0];
193 assert_eq!(record.original_bytes().unwrap(), vec![0x75, 0x05]);
194 assert_eq!(record.patched_bytes().unwrap(), vec![0x90, 0x90]);
195 }
196}