onerom_database/lib.rs
1//! One ROM Lab - ROM Database
2//!
3//! The primary approach taken to identifying ROMs is to use a SHA1 digest of
4//! it. If that fails to provide a match, we try a 32-bit summing checksum.
5//!
6//! This combination will always, uniquely, identify a ROM - in fact the SHA1
7//! digest should. This has the side effect of a unique ROM image needing a
8//! single name and part number in the database. If this turns out to be an
9//! unsound assumption, we may need to add the concept of aliases.
10
11// Copyright (c) 2025 Piers Finlayson <piers@piers.rocks>
12//
13// MIT licence
14
15#![no_std]
16
17extern crate alloc;
18
19#[allow(unused_imports)]
20use log::{debug, error, info, trace, warn};
21
22use alloc::vec::Vec;
23use core::num::Wrapping;
24use hex_literal::hex;
25use sha1::{Digest, Sha1};
26
27pub mod types;
28pub use types::{CsActive, RomType};
29
30// Known ROM database
31//
32// - ROMs are in `roms/*.csv` files
33// - See `scripts/url_checksum.py` for the script that generated the checksums
34// and SHA1 digests
35include!(concat!(env!("OUT_DIR"), "/roms.rs"));
36
37/// Type agonostic wrapping checksum function. We typically only use the u32
38/// version, as u16 and u8 values are the same, with the top bytes masked off.
39pub fn checksum<T>(data: &[u8]) -> T
40where
41 T: Default + From<u8> + Copy,
42 Wrapping<T>: core::ops::Add<Output = Wrapping<T>>,
43{
44 let mut checksum = Wrapping(T::default());
45 for &byte in data {
46 checksum = checksum + Wrapping(T::from(byte));
47 }
48 checksum.0
49}
50
51pub fn sha1_digest(data: &[u8]) -> [u8; 20] {
52 let mut hasher = Sha1::new();
53 hasher.update(data);
54 let result = hasher.finalize();
55 let mut sha1 = [0u8; 20];
56 sha1.copy_from_slice(&result);
57 sha1
58}
59
60/// A ROM database entry
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct RomEntry {
63 // Human readable name for this ROM
64 name: &'static str,
65
66 // Part number for this ROM image, likely assigned by the OEM to the
67 // original chip
68 part: &'static str,
69
70 // Wrapping 32-bit checksum of the ROM image. If you require 16-bit or
71 // 8-bit simply mask off the unnecessary bytes.
72 sum: u32,
73
74 // SHA1 digest for this ROM image. In reality likely to be unique, except
75 // where the same image is used for different ROM types (i.e. chip select
76 // behaviour).
77 sha1: [u8; 20],
78
79 // The ROM type - both the model (2364/2332/2316) and the chip select line
80 // behaviour.
81 rom_type: RomType,
82}
83
84impl RomEntry {
85 const fn new(
86 name: &'static str,
87 part: &'static str,
88 sum: u32,
89 sha1: [u8; 20],
90 rom_type: RomType,
91 ) -> Self {
92 Self {
93 name,
94 part,
95 sum,
96 sha1,
97 rom_type,
98 }
99 }
100
101 /// Returns whether the given checksum matches this ROM image.
102 pub fn matches_checksum(&self, sum: u32) -> bool {
103 self.sum == sum
104 }
105
106 /// Returns whether the given SHA1 matches this ROM image.
107 pub fn matches_sha1(&self, sha1: &[u8; 20]) -> bool {
108 self.sha1 == *sha1
109 }
110
111 /// Returns the human readable name for this ROM.
112 pub fn name(&self) -> &'static str {
113 self.name
114 }
115
116 /// Returns the part number for this ROM, likely assigned by the OEM to
117 /// the original chip
118 pub fn part(&self) -> &'static str {
119 self.part
120 }
121
122 /// Returns wrapping 8-bit checksum of the ROM image.
123 pub fn sum8(&self) -> u8 {
124 (self.sum & 0xFF) as u8
125 }
126
127 /// Returns wrapping 16-bit checksum of the ROM image.
128 pub fn sum16(&self) -> u16 {
129 (self.sum & 0xFFFF) as u16
130 }
131
132 /// Returns wrapping 32-bit checksum of the ROM image.
133 pub fn sum(&self) -> u32 {
134 self.sum
135 }
136
137 /// Returns the SHA1 digest for this ROM image.
138 pub fn sha1(&self) -> &[u8; 20] {
139 &self.sha1
140 }
141
142 /// Returns the ROM type for this ROM image. Includes IC type (2364,
143 /// 2332, 2316) and chip select line behaviour.
144 pub fn rom_type(&self) -> RomType {
145 self.rom_type
146 }
147}
148
149#[allow(dead_code)]
150fn identify_rom_checksum(sum: u32) -> impl Iterator<Item = &'static RomEntry> {
151 ROMS.iter().filter(move |rom| rom.matches_checksum(sum))
152}
153
154fn identify_rom_sha1(sha1: &[u8; 20]) -> impl Iterator<Item = &'static RomEntry> {
155 ROMS.iter().filter(move |rom| rom.matches_sha1(sha1))
156}
157
158/// Function to identify a ROM by SHA1. We do not do checksum matching at all
159/// because clashes are likely, and it complicates our logic (we have to only
160/// do checksum matching if SHA1 fails for _all_ ROM types).
161///
162/// Returns a tuple of matching ROM entries and those that matched, but with
163/// the wrong type. These "bad" matches are likely due to chip select line
164/// differences between the ROM tested and the information in the database.
165pub fn identify_rom(
166 rom_type: &RomType,
167 _sum: u32,
168 sha1: [u8; 20],
169) -> (Vec<&'static RomEntry>, Vec<(&'static RomEntry, RomType)>) {
170 let candidates = identify_rom_sha1(&sha1).collect::<Vec<_>>();
171
172 let mut matches = Vec::new();
173 let mut wrong_type_matches = Vec::new();
174
175 for entry in candidates {
176 if entry.rom_type == *rom_type {
177 matches.push(entry);
178 } else {
179 wrong_type_matches.push((entry, *rom_type));
180 }
181 }
182
183 (matches, wrong_type_matches)
184}
185
186/// Database errors
187pub enum Error {
188 /// Failed to parse the buffer into the requested type
189 ParseError,
190}