import json import unittest from h8536.model import Instruction from h8536.serial_reconstruction import ( analyze_serial_reconstruction, serial_reconstruction_comment_for_instruction, serial_reconstruction_json_payload, serial_reconstruction_metadata_for_instruction, ) def ins( address: int, mnemonic: str, operands: str = "", references: list[int] | None = None, kind: str = "normal", targets: list[int] | None = None, ) -> Instruction: return Instruction( address, b"", mnemonic, operands, kind=kind, targets=targets or [], references=references or [], ) class SerialReconstructionTest(unittest.TestCase): def test_candidate_sci1_tx_frame_length_and_checksum_pattern(self): instructions = { 0x3000: ins(0x3000, "MOV:G.B", "#H'02, @H'F858", [0xF858]), 0x3004: ins(0x3004, "MOV:G.B", "@H'F858, R0", [0xF858]), 0x3008: ins(0x3008, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]), 0x300C: ins(0x300C, "MOV:G.B", "#1, @H'F9C2", [0xF9C2]), 0x301C: ins(0x301C, "MOV:E.B", "#H'5A, R1", []), 0x3020: ins(0x3020, "XOR.B", "@H'F858, R1", [0xF858]), 0x3024: ins(0x3024, "XOR.B", "@H'F859, R1", [0xF859]), 0x3028: ins(0x3028, "XOR.B", "@H'F85A, R1", [0xF85A]), 0x302C: ins(0x302C, "XOR.B", "@H'F85B, R1", [0xF85B]), 0x3030: ins(0x3030, "XOR.B", "@H'F85C, R1", [0xF85C]), 0x3034: ins(0x3034, "MOV:G.B", "R1, @H'F85D", [0xF85D]), 0x3100: ins(0x3100, "MOV:G.B", "@H'F9C2, R2", [0xF9C2]), 0x3104: ins(0x3104, "MOV:G.B", "@(-H'07A8,R2), R0", []), 0x3108: ins(0x3108, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]), 0x310C: ins(0x310C, "ADD:Q.B", "#1, @H'F9C2", [0xF9C2]), 0x3110: ins(0x3110, "CMP:G.B", "#6, @H'F9C2", [0xF9C2]), } analysis = analyze_serial_reconstruction(instructions) self.assertEqual(len(analysis["candidates"]), 1) candidate = analysis["candidates"][0] self.assertEqual(candidate["kind"], "candidate_sci1_tx_frame") self.assertEqual(candidate["channel"], "SCI1") self.assertEqual(candidate["frame_length"], 6) self.assertEqual(candidate["buffer_start"], 0xF858) self.assertEqual(candidate["checksum_address"], 0xF85D) self.assertEqual(candidate["tx_index_address"], 0xF9C2) self.assertEqual(candidate["checksum_seed"], 0x5A) self.assertEqual(candidate["confidence"], "high") self.assertEqual(candidate["confidence_score"], 0.95) self.assertEqual(candidate["missing_evidence"], []) evidence_addresses = candidate["evidence_addresses"] self.assertIn(0x3034, evidence_addresses["checksum_byte"]) self.assertIn(0x3034, evidence_addresses["xor_checksum_chain"]) self.assertIn(0x3108, evidence_addresses["tx_isr_indexed_send"]) self.assertEqual(evidence_addresses["tx_index_compare_frame_length"], [0x3110]) self.assertEqual(evidence_addresses["tx_checksum_seed"], [0x301C]) checksum_comment = serial_reconstruction_comment_for_instruction(analysis, 0x3034) self.assertIn("candidate/evidence-supported SCI1 6-byte TX frame", checksum_comment) self.assertIn("checksum H'F85D", checksum_comment) self.assertIn("confidence high", checksum_comment) metadata = serial_reconstruction_metadata_for_instruction(analysis, 0x3108) self.assertEqual(metadata[0]["candidate_id"], "sci1_tx_frame_f858_len6_candidate") self.assertEqual(metadata[0]["evidence"], "tx_isr_indexed_send") self.assertIn(0x3108, metadata[0]["evidence_addresses"]) payload = serial_reconstruction_json_payload(analysis) self.assertEqual(payload["candidates"][0]["frame_length"], 6) json.dumps(payload) def test_candidate_sci1_rx_frame_length_and_checksum_validation_pattern(self): instructions = { 0x4FF0: ins(0x4FF0, "BSET.B", "#7, @H'FAA4", [0xFAA4]), 0x4FF4: ins(0x4FF4, "BCLR.B", "#5, @SCI1_SSR", [0xFEDC]), 0x4FF8: ins(0x4FF8, "BCLR.B", "#4, @SCI1_SSR", [0xFEDC]), 0x4FFC: ins(0x4FFC, "BCLR.B", "#3, @SCI1_SSR", [0xFEDC]), 0x4FFE: ins(0x4FFE, "BCLR.B", "#6, @SCI1_SSR", [0xFEDC]), 0x5000: ins(0x5000, "MOV:G.B", "@SCI1_RDR, R0", [0xFEDD]), 0x5004: ins(0x5004, "MOV:G.B", "R0, @(-H'0798,R1)", []), 0x5008: ins(0x5008, "ADD:Q.B", "#1, R1", []), 0x500C: ins(0x500C, "MOV:G.B", "R1, @H'F9C3", [0xF9C3]), 0x5010: ins(0x5010, "CMP:E", "#H'06, R1", []), 0x5014: ins(0x5014, "MOV:G.B", "#H'14, @H'F9C5", [0xF9C5]), 0x5100: ins(0x5100, "CMP:G.B", "#H'06, @H'F9C3", [0xF9C3]), 0x5104: ins(0x5104, "MOV:G.W", "@H'F868, R0", [0xF868]), 0x5108: ins(0x5108, "MOV:G.W", "R0, @H'F860", [0xF860]), 0x510C: ins(0x510C, "MOV:G.W", "@H'F86A, R0", [0xF86A]), 0x5110: ins(0x5110, "MOV:G.W", "R0, @H'F862", [0xF862]), 0x5114: ins(0x5114, "MOV:G.W", "@H'F86C, R0", [0xF86C]), 0x5118: ins(0x5118, "MOV:G.W", "R0, @H'F864", [0xF864]), 0x5120: ins(0x5120, "MOV:E.B", "#H'5A, R0", []), 0x5124: ins(0x5124, "XOR.B", "@H'F860, R0", [0xF860]), 0x5128: ins(0x5128, "XOR.B", "@H'F861, R0", [0xF861]), 0x512C: ins(0x512C, "XOR.B", "@H'F862, R0", [0xF862]), 0x5130: ins(0x5130, "XOR.B", "@H'F863, R0", [0xF863]), 0x5134: ins(0x5134, "XOR.B", "@H'F864, R0", [0xF864]), 0x5138: ins(0x5138, "CMP:G.B", "@H'F865, R0", [0xF865]), } analysis = analyze_serial_reconstruction(instructions) candidate = next(item for item in analysis["candidates"] if item["kind"] == "candidate_sci1_rx_frame") self.assertEqual(candidate["frame_length"], 6) self.assertEqual(candidate["capture_buffer_start"], 0xF868) self.assertEqual(candidate["validation_buffer_start"], 0xF860) self.assertEqual(candidate["checksum_address"], 0xF865) self.assertEqual(candidate["checksum_seed"], 0x5A) self.assertIn("no explicit header", candidate["confidence_reason"]) self.assertIn("rx_rdrf_clear_before_rdr_read", candidate["evidence_addresses"]) self.assertIn("rx_eri_falls_through_to_rxi", candidate["evidence_addresses"]) self.assertTrue(candidate["rx_error_handling"]["fallthrough_to_rx_byte_path"]) self.assertTrue(candidate["rx_error_handling"]["rdrf_clear_before_rdr_read"]) self.assertEqual(candidate["rx_error_handling"]["error_latch_address"], 0xFAA4) comment = serial_reconstruction_comment_for_instruction(analysis, 0x5138) self.assertIn("candidate/evidence-supported SCI1 6-byte RX frame", comment) self.assertIn("checksum H'F865", comment) self.assertIn("confidence high", comment) self.assertIn( "ROM clears SCI1 SSR.RDRF before reading SCI1_RDR", serial_reconstruction_comment_for_instruction(analysis, 0x4FFE), ) self.assertIn( "SCI1 ERI latches FAA4.bit7", serial_reconstruction_comment_for_instruction(analysis, 0x4FF0), ) def test_lone_tdr_write_does_not_emit_reconstruction(self): instructions = { 0x4000: ins(0x4000, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]), } analysis = analyze_serial_reconstruction(instructions) self.assertEqual(analysis["candidates"], []) self.assertEqual(serial_reconstruction_comment_for_instruction(analysis, 0x4000), "") self.assertEqual(serial_reconstruction_metadata_for_instruction(analysis, 0x4000), []) self.assertEqual(serial_reconstruction_json_payload(analysis)["candidates"], []) if __name__ == "__main__": unittest.main()