Skip to main content

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}