1use std::cmp::min;
2use std::convert::TryInto;
3use std::ffi::OsString;
4use std::fs::File;
5use std::io::{Read, Write};
6use std::path::Path;
7
8use anyhow::{bail, Result};
9
10use hmac_sha256::Hash;
11
12mod cdrom;
13
14use crate::cdrom::CDRomImage;
15
16pub const DESIRED_SATURN_DISC: SaturnDisc = SaturnDisc {
18 desired_region_bytes: *br"JUBLKTEA",
20 desired_mfr_bytes: *br"SEGA TP T-81 ",
22 };
25
26const SEGA_SATURN_BYTES: &[u8; 16] = br"SEGA SEGASATURN ";
28
29const BACKUP_FILE_EXT: &str = ".saturnpatchbak";
30
31const REGION_CODES: &[u8; 8] = br"JTUBKAEL";
33
34const REGION_STRING_LEN: usize = 32;
35
36const REGION_STRINGS: &[&[u8; REGION_STRING_LEN]] = &[
37 b"\xA0\x0E\x00\x09For JAPAN. ",
38 b"\xA0\x0E\x00\x09For TAIWAN and PHILIPINES. ",
39 b"\xA0\x0E\x00\x09For USA and CANADA. ",
40 b"\xA0\x0E\x00\x09For BRAZIL. ",
41 b"\xA0\x0E\x00\x09For KOREA. ",
42 b"\xA0\x0E\x00\x09For ASIA PAL area. ",
43 b"\xA0\x0E\x00\x09For EUROPE. ",
44 b"\xA0\x0E\x00\x09For LATIN AMERICA. ",
45];
46
47const MIN_BACKUP_SIZE: usize = 1 + 32 + 1;
49
50fn region_index(region: &u8) -> Result<usize> {
51 let mut i = 0;
52 for c in REGION_CODES {
53 if c == region {
54 return Ok(i);
55 }
56 i += 1;
57 }
58 bail!("invalid region: {}", *region as char);
59}
60
61fn region_copy_sort_pad(regions: &[u8]) -> Vec<u8> {
62 let mut ret = regions.to_vec();
63 ret.sort_by(|a, b| region_index(a).unwrap_or(10).cmp(®ion_index(b).unwrap_or(10)));
64 while ret.len() < 16 {
65 ret.push(b' ');
66 }
67 ret
68}
69
70fn region_count(regions: &[u8]) -> usize {
71 let mut ret = 0;
72 while ret < regions.len() {
73 if regions[ret] == b' ' {
74 return ret;
75 }
76 ret += 1;
77 }
78 ret
79}
80
81fn first_index_of(file_name: &OsString, haystack: &[u8], needle: &[u8]) -> Result<usize> {
82 for i in 0..haystack.len() - needle.len() + 1 {
83 if haystack[i..i + needle.len()] == needle[..] {
84 return Ok(i);
85 }
86 }
87 bail!("not saturn image? {:?}", file_name);
88}
89
90pub struct SaturnDisc {
91 pub desired_region_bytes: [u8; 8],
92 pub desired_mfr_bytes: [u8; 16],
93}
94
95impl SaturnDisc {
96 pub fn from_env_args() -> Result<SaturnDisc> {
97 let desired_mfr_bytes = match std::env::var("SATURN_MANUFACTURER") {
98 Ok(k) => {
99 let mfr_bytes = k.as_bytes();
100 if mfr_bytes.len() == 0 {
101 DESIRED_SATURN_DISC.desired_mfr_bytes
102 } else {
103 if mfr_bytes.len() > 16 {
104 bail!("SATURN_MANUFACTURER length {} exceeds max length of 16", mfr_bytes.len());
105 }
106 let mut ret = [20u8; 16];
107 ret[0..mfr_bytes.len()].copy_from_slice(mfr_bytes);
108 ret
109 }
110 }
111 Err(_) => DESIRED_SATURN_DISC.desired_mfr_bytes,
112 };
113
114 let desired_region_bytes = match std::env::var("SATURN_REGION") {
115 Ok(k) => {
116 let k = k.to_ascii_uppercase();
117 let region_bytes = k.as_bytes();
118 if region_bytes.len() == 0 {
119 DESIRED_SATURN_DISC.desired_region_bytes
120 } else {
121 if region_bytes.len() > 8 {
122 bail!("SATURN_REGION length {} exceeds max length of 8", region_bytes.len());
123 }
124 for region in region_bytes {
126 region_index(region)?;
127 }
128 {
129 let mut region_vec = region_bytes.to_vec();
131 region_vec.sort();
132 region_vec.dedup();
133 if region_bytes.len() != region_bytes.len() {
134 bail!("SATURN_REGION must not have duplicate regions");
135 }
136 }
137 let mut empty = [20u8; 8];
138 empty[0..region_bytes.len()].copy_from_slice(region_bytes);
139 if region_bytes.len() != 8 {
140 let mut x = region_bytes.len();
142 REGION_CODES.iter().filter(|r| !region_bytes.contains(r)).for_each(|r| {
143 empty[x] = *r;
144 x += 1;
145 });
146 }
147 println!("SATURN_REGION: {}", String::from_utf8_lossy(&empty));
148 empty
149 }
150 }
151 Err(_) => DESIRED_SATURN_DISC.desired_region_bytes,
152 };
153
154 Ok(SaturnDisc {
155 desired_region_bytes,
156 desired_mfr_bytes,
157 })
158 }
159
160 pub fn patch(&self, file_name: &OsString) -> Result<()> {
161 let path = Path::new(file_name);
162 if !path.is_file() {
163 bail!("file does not exist: {:?}", file_name);
164 }
165 let mut file = File::open(file_name)?;
166 let mut bytes = Vec::new();
167 file.read_to_end(&mut bytes)?;
168
169 let header_offset = first_index_of(file_name, &bytes[0..min(bytes.len(), 256)], SEGA_SATURN_BYTES)?;
171
172 let cdrom = CDRomImage::new(&bytes)?;
173
174 let orig_hash = Hash::hash(&bytes);
175 cdrom.update_sectors(&mut bytes);
177 if orig_hash != Hash::hash(&bytes) {
178 bail!("existing CD has bad sectors? refusing to update because won't be able to restore original file: {:?}", file_name);
179 }
180
181 let change_mfr = self.desired_mfr_bytes != &bytes[(header_offset + 16)..(header_offset + 32)];
182 let region_bytes = &bytes[(header_offset + 64)..(header_offset + 80)];
183 let region_count = region_count(®ion_bytes);
184 let new_region = region_copy_sort_pad(&self.desired_region_bytes[0..region_count]);
185 let change_region = new_region != region_bytes;
186 let region_bytes = ®ion_bytes[0..region_count].to_vec();
188
189 if change_mfr || change_region {
190 let mut first_region_string_index = bytes.len();
192 let mut region_string_indices = Vec::with_capacity(region_count);
193 let mut string_begin = header_offset + 80 + 16; let mut string_end = bytes.len();
195 for i in 0..region_count {
197 let region_string = REGION_STRINGS[region_index(®ion_bytes[i])?];
199 let string_offset = first_index_of(file_name, &bytes[string_begin..string_end], region_string)? + string_begin;
201 if i == 0 && region_count > 1 {
202 string_begin = string_offset - ((region_count - 1) * REGION_STRING_LEN);
205 string_end = string_begin + (region_count * REGION_STRING_LEN * 2);
207 }
208 if string_offset < first_region_string_index {
209 first_region_string_index = string_offset;
210 }
211 region_string_indices.push(string_offset);
212 }
213
214 let mut backup_file_path = file_name.clone();
215 backup_file_path.push(BACKUP_FILE_EXT);
216 let backup_file_path = Path::new(&backup_file_path);
217 let write_header = !backup_file_path.is_file();
218 let mut backup_vec = Vec::new();
219 if write_header {
220 backup_vec.write_all(&orig_hash)?;
223 backup_vec.push(region_count as u8);
225 backup_vec.write_all(&(first_region_string_index as u32).to_be_bytes())?;
227 backup_vec.write_all(&bytes[(header_offset + 16)..(header_offset + 32)])?;
229 backup_vec.write_all(&bytes[(header_offset + 64)..(header_offset + 80)])?;
231 backup_vec.write_all(&bytes[first_region_string_index..(first_region_string_index + (region_count * REGION_STRING_LEN))])?;
233 }
234
235 if change_mfr {
236 &bytes[(header_offset + 16)..(header_offset + 32)].copy_from_slice(&self.desired_mfr_bytes);
237 }
238
239 if change_region {
240 &bytes[(header_offset + 64)..(header_offset + 80)].copy_from_slice(&new_region);
241 for i in 0..region_count {
246 let new_region_string = REGION_STRINGS[region_index(&new_region[i])?];
247 let string_offset = region_string_indices[i];
248 &bytes[string_offset..(string_offset + REGION_STRING_LEN)].copy_from_slice(new_region_string);
249 }
251 }
252
253 cdrom.update_sectors(&mut bytes);
254
255 if write_header {
256 let mut backup_file = File::create(backup_file_path)?;
258 backup_file.write_all(&[0])?;
260 backup_file.write_all(&Hash::hash(&backup_vec))?;
262 backup_file.write_all(&backup_vec)?;
264 drop(backup_vec); }
266
267 let mut file = File::create(file_name)?;
268 file.write_all(&bytes)?;
269
270 print!("SUCCESS: ");
271 if write_header {
272 print!("wrote header backup, ");
273 }
274 if change_mfr {
275 print!("changed manufacturer, ");
276 }
277 if change_region {
278 print!("changed regions, ");
279 }
280
281 println!("patched: {:?}", file_name);
282 } else {
283 println!("SUCCESS: already desired manufacturer and regions {:?}", file_name);
284 }
285
286 Ok(())
287 }
288
289 pub fn unpatch(file_name: &OsString) -> Result<()> {
290 let path = Path::new(file_name);
291 if !path.is_file() {
292 bail!("file does not exist: {:?}", file_name);
293 }
294
295 let (file_name, header_backup_str) = if path.extension().map_or(false, |ext| ext.eq(&BACKUP_FILE_EXT[1..])) {
296 let mut new_path = path.to_path_buf();
298 new_path.set_file_name(path.file_stem().expect("no file name? impossible without extension I think..."));
299 (new_path.into_os_string(), file_name.to_owned())
300 } else {
301 let mut header_backup_str = file_name.clone();
302 header_backup_str.push(BACKUP_FILE_EXT);
303 (file_name.to_owned(), header_backup_str)
304 };
305
306 let path = Path::new(&file_name);
307 if !path.is_file() {
308 bail!("file {:?} does not exist for backup file {:?}", file_name, header_backup_str);
309 }
310
311 let header_backup = Path::new(&header_backup_str);
312 if !header_backup.is_file() {
313 bail!("backup file {:?} does not exist for file {:?}", header_backup_str, file_name);
314 }
315
316 let mut file = File::open(path)?;
317 let mut bytes = Vec::new();
318 file.read_to_end(&mut bytes)?;
319
320 let mut header_backup = File::open(header_backup)?;
321 let mut header_bytes = Vec::new();
322 header_backup.read_to_end(&mut header_bytes)?;
323
324 if header_bytes.len() < MIN_BACKUP_SIZE {
325 bail!(
326 "backup file {:?} length {} is less than minimum length of {} for file {:?}",
327 header_backup_str,
328 header_bytes.len(),
329 MIN_BACKUP_SIZE,
330 file_name
331 );
332 }
333 if header_bytes[0] != 0 {
334 bail!("corrupt backup file or unsupported version {}, only version 0 supported: {:?}", header_bytes[0], header_backup_str);
335 }
336 if header_bytes[1..33] != Hash::hash(&header_bytes[33..]) {
337 bail!("corrupt backup file, hash mismatch: {:?}", header_backup_str);
338 }
339 let header_bytes = &header_bytes[33..];
341
342 let header_offset = first_index_of(&file_name, &bytes[0..min(bytes.len(), 256)], SEGA_SATURN_BYTES)?;
344
345 let cdrom = CDRomImage::new(&bytes)?;
346
347 let orig_hash = &header_bytes[0..32];
349 let region_count = header_bytes[32] as usize;
350 let first_region_string_index = u32::from_be_bytes(header_bytes[33..37].try_into()?) as usize;
351 let header_bytes = &header_bytes[37..];
353 let manufacturer = &header_bytes[0..16];
354 let regions = &header_bytes[16..32];
355 let region_strings = &header_bytes[32..];
356
357 if &bytes[(header_offset + 16)..(header_offset + 32)] != manufacturer
358 || &bytes[(header_offset + 64)..(header_offset + 80)] != regions
359 || &bytes[first_region_string_index..(first_region_string_index + (region_count * REGION_STRING_LEN))] != region_strings
360 {
361 &bytes[(header_offset + 16)..(header_offset + 32)].copy_from_slice(manufacturer);
362 &bytes[(header_offset + 64)..(header_offset + 80)].copy_from_slice(regions);
363 &bytes[first_region_string_index..(first_region_string_index + (region_count * REGION_STRING_LEN))].copy_from_slice(region_strings);
364
365 cdrom.update_sectors(&mut bytes);
366
367 if orig_hash != Hash::hash(&bytes) {
368 bail!("restore failed, hash mismatch: {:?}", file_name);
369 }
370
371 let mut file = File::create(&file_name)?;
372 file.write_all(&bytes)?;
373 file.flush()?;
374 drop(file);
375
376 std::fs::remove_file(header_backup_str).ok();
379
380 println!("SUCCESS: unpatched: {:?}", file_name);
381 } else if orig_hash == Hash::hash(&bytes) {
382 std::fs::remove_file(header_backup_str).ok();
384
385 println!("SUCCESS: already unpatched: {:?}", file_name);
386 } else {
387 bail!("restore failed, unknown problem: {:?}", file_name);
388 }
389
390 Ok(())
391 }
392}