Skip to content

Commit 83a547a

Browse files
Add TSCrunch packer strategy and test cases
Implement TSCrunch v1.3+ and v1.3+-X2 signature detection and post-emulation end address calculation. Fix CCS signature precision and align packer detection ordering with unp64. Add 4 TSCrunch PRG files to unpacker tests and unpacker_compare_all suite with explicit packer name assertions.
1 parent 0f8af8e commit 83a547a

9 files changed

Lines changed: 248 additions & 37 deletions

File tree

crates/regenerator2000-core/src/bin/unpacker_compare_all.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,19 @@ use std::process::Command;
55
fn main() {
66
let unp64_bin = std::env::var("UNP64")
77
.or_else(|_| std::env::var("UNP64_PATH"))
8-
.unwrap_or_else(|_| "/Users/ricardoq/bin/unp64".to_string());
8+
.unwrap_or_else(|_| {
9+
let candidates = [
10+
"/Users/ricardoq/.local/bin/unp64",
11+
"/Users/ricardoq/bin/unp64",
12+
"unp64",
13+
];
14+
for c in candidates {
15+
if std::path::Path::new(c).exists() {
16+
return c.to_string();
17+
}
18+
}
19+
"unp64".to_string()
20+
});
921
let test_dir = "tests/6502";
1022

1123
let allowed_files = [
@@ -14,6 +26,8 @@ fn main() {
1426
"c64_connection-8580.pucrunch.prg",
1527
"c64_f600.exo.prg",
1628
"c64_moving_tubes_lxt.dali.prg",
29+
"c64_moving_tubes_lxt.tscrunch_x.prg",
30+
"c64_moving_tubes_lxt.tscrunch_x2.prg",
1731
"c64_thats_the_way_scoop.time_cruncher.prg",
1832
"c64_traveller.tiny_crunch.prg",
1933
"c64_CopperBooze.byte_boozer2.prg",
@@ -30,6 +44,8 @@ fn main() {
3044
"c64_mule.exo3.prg",
3145
"c64_mule.mccracken_compressor.prg",
3246
"c64_mule.pucrunch.prg",
47+
"c64_mule.tscrunch_x.prg",
48+
"c64_mule.tscrunch_x2.prg",
3349
"c64_roma.exe.exo3.prg",
3450
"c64_hw20131031.exo.prg",
3551
"c64_spectro.exo3.prg",

crates/regenerator2000-core/src/packers/ccs.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@ use super::{Packer, PackerInfo};
77
#[must_use]
88
pub fn detect(mem: &[u8], load_addr: u16) -> Option<Box<dyn Packer>> {
99
if mem.len() > 0x81C && load_addr <= 0x0801 {
10-
let q = 0x080D;
11-
if q >= load_addr as usize
12-
&& mem[q] == 0x78
13-
&& mem[q + 1] == 0xA9
14-
&& mem[q + 2] == 0x34
15-
&& mem[q + 3] == 0x85
16-
&& mem[q + 4] == 0x01
17-
&& mem[q + 5] == 0xA2
18-
&& mem[q + 7] == 0xBD
19-
&& mem[q + 0x0A] == 0x9D
20-
&& mem[q + 0x0D] == 0xCA
21-
&& mem[q + 0x0E] == 0xD0
22-
{
10+
// CCS signatures:
11+
// Variant 1: SEI at $0817 followed by INC $01, LDA ... ($78, $E6, $01, $B9)
12+
let is_v1 = mem.len() > 0x81B
13+
&& mem[0x817 - load_addr as usize..].starts_with(&[0x78, 0xE6, 0x01, 0xB9]);
14+
// Variant 2: LDY #$00, SEI, INC $01 at $0812 ($A0, $00, $78, $E6)
15+
let is_v2 = mem.len() > 0x816
16+
&& mem[0x812 - load_addr as usize..].starts_with(&[0xA0, 0x00, 0x78, 0xE6]);
17+
// Variant 3: SEI, INC $01, LDA at $0814 ($78, $E6, $01, $B9)
18+
let is_v3 = mem.len() > 0x818
19+
&& mem[0x814 - load_addr as usize..].starts_with(&[0x78, 0xE6, 0x01, 0xB9]);
20+
// Variant 4: LDY #$00, SEI, STY at $080B ($A0, $00, $78, $8C)
21+
let is_v4 = mem.len() > 0x80F
22+
&& mem[0x80B - load_addr as usize..].starts_with(&[0xA0, 0x00, 0x78, 0x8C]);
23+
24+
if is_v1 || is_v2 || is_v3 || is_v4 {
2325
return Some(Box::new(SimplePacker::new(PackerInfo {
2426
name: "Commodore Cruncher System",
2527
dep_addr: Some(0x0100),

crates/regenerator2000-core/src/packers/mod.rs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub mod tbc;
1919
pub mod time_cruncher;
2020
pub mod tiny_crunch;
2121
pub mod triad;
22+
pub mod tscrunch;
2223
pub mod turbo_cruncher;
2324

2425
use mos6502::cpu::CPU;
@@ -83,63 +84,69 @@ pub trait Packer: Debug + Send + Sync {
8384
}
8485

8586
/// Scans memory for known packer signatures and returns a corresponding [`Packer`] strategy if found.
87+
///
88+
/// **Note:** Detection order matters to avoid false positives between packers with overlapping
89+
/// signatures. The detection order here follows the scanner order established in `unp64` (`_Scanners.c`).
8690
#[must_use]
8791
pub fn detect_packer(mem: &[u8], load_addr: u16, load_end: u16) -> Option<Box<dyn Packer>> {
88-
if let Some(p) = exomizer::detect(mem, load_addr, load_end) {
92+
if let Some(p) = eca::detect(mem) {
8993
return Some(p);
9094
}
91-
if let Some(p) = pucrunch::detect(mem) {
95+
if let Some(p) = action_replay::detect(mem, load_addr) {
9296
return Some(p);
9397
}
94-
if let Some(p) = time_cruncher::detect(mem) {
98+
if let Some(p) = cruel_cruncher::detect(mem, load_addr) {
9599
return Some(p);
96100
}
97-
if let Some(p) = dali::detect(mem, load_addr) {
101+
if let Some(p) = pucrunch::detect(mem) {
98102
return Some(p);
99103
}
100-
if let Some(p) = byteboozer::detect(mem, load_addr) {
104+
if let Some(p) = time_cruncher::detect(mem) {
101105
return Some(p);
102106
}
103-
if let Some(p) = tiny_crunch::detect(mem, load_addr) {
107+
if let Some(p) = mc_cracken::detect(mem) {
104108
return Some(p);
105109
}
106-
if let Some(p) = cruel_cruncher::detect(mem, load_addr) {
110+
if let Some(p) = tbc::detect(mem) {
107111
return Some(p);
108112
}
109-
if let Some(p) = ccs::detect(mem, load_addr) {
113+
if let Some(p) = byteboozer::detect(mem, load_addr) {
110114
return Some(p);
111115
}
112-
if let Some(p) = turbo_cruncher::detect(mem, load_addr) {
116+
if let Some(p) = alz64::detect(mem, load_addr) {
113117
return Some(p);
114118
}
115-
if let Some(p) = action_replay::detect(mem, load_addr) {
119+
if let Some(p) = tscrunch::detect(mem, load_addr) {
116120
return Some(p);
117121
}
118-
if let Some(p) = final_cartridge::detect(mem, load_addr) {
122+
if let Some(p) = dali::detect(mem, load_addr) {
119123
return Some(p);
120124
}
121-
if let Some(p) = triad::detect(mem, load_addr) {
125+
if let Some(p) = tiny_crunch::detect(mem, load_addr) {
122126
return Some(p);
123127
}
124-
if let Some(p) = eagle::detect(mem, load_addr) {
128+
if let Some(p) = super_cruncher::detect(mem, load_addr) {
125129
return Some(p);
126130
}
127-
if let Some(p) = super_cruncher::detect(mem, load_addr) {
131+
if let Some(p) = final_cartridge::detect(mem, load_addr) {
128132
return Some(p);
129133
}
130-
if let Some(p) = alz64::detect(mem, load_addr) {
134+
if let Some(p) = exomizer::detect(mem, load_addr, load_end) {
131135
return Some(p);
132136
}
133-
if let Some(p) = tbc::detect(mem) {
137+
if let Some(p) = ccs::detect(mem, load_addr) {
134138
return Some(p);
135139
}
136-
if let Some(p) = eca::detect(mem) {
140+
if let Some(p) = triad::detect(mem, load_addr) {
141+
return Some(p);
142+
}
143+
if let Some(p) = eagle::detect(mem, load_addr) {
137144
return Some(p);
138145
}
139146
if let Some(p) = antiram::detect(mem) {
140147
return Some(p);
141148
}
142-
if let Some(p) = mc_cracken::detect(mem) {
149+
if let Some(p) = turbo_cruncher::detect(mem, load_addr) {
143150
return Some(p);
144151
}
145152

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//! TSCrunch strategy implementation.
2+
3+
use super::{Packer, PackerInfo};
4+
use crate::state::types::System;
5+
6+
/// TSCrunch packer implementation with post-emulation end address recovery.
7+
#[derive(Debug, Clone, PartialEq, Eq)]
8+
pub struct TScrunchPacker {
9+
/// Information describing the packer.
10+
pub info: PackerInfo,
11+
/// Zero-page address holding the destination end pointer (low byte).
12+
pub zp_end_ptr: u16,
13+
}
14+
15+
impl Packer for TScrunchPacker {
16+
fn info(&self) -> PackerInfo {
17+
self.info.clone()
18+
}
19+
20+
fn post_emulate(
21+
&self,
22+
mem: &[u8],
23+
_snapshot: &[u8],
24+
_written: &[bool],
25+
range: &mut (u16, u16),
26+
_entry_point: &mut u16,
27+
_system: &System,
28+
_y_reg: u8,
29+
) {
30+
let ptr = self.zp_end_ptr as usize;
31+
if mem.len() >= ptr + 2 {
32+
let end_addr = u16::from_le_bytes([mem[ptr], mem[ptr + 1]]);
33+
if end_addr > range.0 {
34+
range.1 = end_addr.saturating_sub(1);
35+
}
36+
}
37+
}
38+
}
39+
40+
/// Detects TSCrunch signature.
41+
#[must_use]
42+
pub fn detect(mem: &[u8], load_addr: u16) -> Option<Box<dyn Packer>> {
43+
if mem.len() > 0x820 && load_addr <= 0x0801 {
44+
let q = 0x080D_usize;
45+
if mem.len() > q + 18 {
46+
// Variant 1: TSCrunch v1.3+ (Zero-Page depacker at $0002)
47+
// Code at $080D: SEI; LDX #$CC; LDA $081A,X; STA $00,X; DEX; BNE; JMP $0002
48+
if mem[q] == 0x78
49+
&& mem[q + 1] == 0xA2
50+
&& mem[q + 3] == 0xBD
51+
&& mem[q + 6] == 0x95
52+
&& mem[q + 7] == 0x00
53+
&& mem[q + 8] == 0xCA
54+
&& mem[q + 9] == 0xD0
55+
&& mem[q + 11] == 0x4C
56+
&& mem[q + 12] == 0x02
57+
&& mem[q + 13] == 0x00
58+
{
59+
let mut entry_point = None;
60+
if mem.len() >= q + 0x70 {
61+
for p in q + 14..mem.len().min(q + 0x80) {
62+
if mem.len() >= p + 8
63+
&& mem[p] == 0xA9
64+
&& mem[p + 1] == 0x37
65+
&& mem[p + 2] == 0x85
66+
&& mem[p + 3] == 0x01
67+
&& mem[p + 4] == 0x58
68+
&& mem[p + 5] == 0x4C
69+
{
70+
entry_point = Some(u16::from_le_bytes([mem[p + 6], mem[p + 7]]));
71+
break;
72+
}
73+
}
74+
}
75+
76+
return Some(Box::new(TScrunchPacker {
77+
info: PackerInfo {
78+
name: "TSCrunch v1.3+",
79+
dep_addr: Some(0x0002),
80+
start_addr: Some(0x0800),
81+
end_addr: None,
82+
entry_point,
83+
end_addr_ptr: Some(0x0026),
84+
},
85+
zp_end_ptr: 0x0026,
86+
}));
87+
}
88+
89+
// Variant 2: TSCrunch v1.3+-X2 (Stack depacker at $0100)
90+
// Code at $080D: SEI; LDA #$34; STA $01; LDX #$D0; LDA $081F,X; STA $00FB,X; DEX; BNE; JMP $0100
91+
if mem[q] == 0x78
92+
&& mem[q + 1] == 0xA9
93+
&& mem[q + 2] == 0x34
94+
&& mem[q + 3] == 0x85
95+
&& mem[q + 4] == 0x01
96+
&& mem[q + 5] == 0xA2
97+
&& mem[q + 7] == 0xBD
98+
&& mem[q + 10] == 0x9D
99+
&& mem[q + 13] == 0xCA
100+
&& mem[q + 14] == 0xD0
101+
&& mem[q + 16] == 0x4C
102+
&& mem[q + 17] == 0x00
103+
&& mem[q + 18] == 0x01
104+
{
105+
let mut entry_point = None;
106+
if mem.len() >= q + 0x70 {
107+
for p in q + 19..mem.len().min(q + 0x80) {
108+
if mem.len() >= p + 8
109+
&& mem[p] == 0xA9
110+
&& mem[p + 1] == 0x37
111+
&& mem[p + 2] == 0x85
112+
&& mem[p + 3] == 0x01
113+
&& mem[p + 4] == 0x58
114+
&& mem[p + 5] == 0x4C
115+
{
116+
entry_point = Some(u16::from_le_bytes([mem[p + 6], mem[p + 7]]));
117+
break;
118+
}
119+
}
120+
}
121+
122+
return Some(Box::new(TScrunchPacker {
123+
info: PackerInfo {
124+
name: "TSCrunch v1.3+-X2",
125+
dep_addr: Some(0x0100),
126+
start_addr: Some(0x0801),
127+
end_addr: None,
128+
entry_point,
129+
end_addr_ptr: Some(0x00FD),
130+
},
131+
zp_end_ptr: 0x00FD,
132+
}));
133+
}
134+
}
135+
}
136+
None
137+
}

0 commit comments

Comments
 (0)