aboutsummaryrefslogtreecommitdiff
path: root/lib/xixanta/src/mapping.rs
blob: b3d22c7c1676ba92dd96f82640ad6033119777e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
use crate::cfg::{parse_cfg_file, parse_nasm_cfg_file};
use crate::object::Bundle;

const EMPTY_CONFIG: &str = include_str!("mappings/empty.cfg");
const NROM_CONFIG: &str = include_str!("mappings/nrom.cfg");
const NROM65_CONFIG: &str = include_str!("mappings/nrom65.cfg");
const UXROM_CONFIG: &str = include_str!("mappings/unrom.cfg");

/// The type of section that a Mapping represents.
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
pub enum SectionType {
    /// The 16 initial bytes describing the header of the ROM file.
    Header,

    /// Bank to be stored in PRG ROM with a size multiple of 8KB.
    PrgRom,

    /// Bank to be stored in CHR ROM with a size multiple of 4KB.
    ChrRom,
}

/// A segment inside of a memory mapping, used to organize the code inside of a
/// given memory mapping. Note that a segment does not do anything else: it's
/// just about organizing code inside of a mapping. It doesn't deal with how to
/// fill a memory region, or where it starts in memory, or anything like that.
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Segment {
    /// Name of the segment.
    pub name: String,

    /// Offset from the base address from where to push the next address for
    /// this segment.
    pub offset: usize,

    /// Bundles that have been generated when assembling the nodes that have
    /// been parsed by a previous step.
    pub bundles: Vec<Bundle>,
}

impl From<&str> for Segment {
    fn from(name: &str) -> Self {
        Segment {
            name: name.to_string(),
            offset: 0,
            bundles: vec![],
        }
    }
}

impl Segment {
    /// Returns the length of the segment by counting the bundles that have been
    // pushed so far into the segment.
    pub fn len(&self) -> usize {
        self.bundles
            .iter()
            .fold(0, |acc, bundle| acc + bundle.size as usize)
    }

    /// Returns true if the given segment has no bundles in it, false otherwise.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

/// A region in memory which has one or more segments in it, which in turn have
/// the bundles that are to be generated in the end of an assembling operation.
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Mapping {
    /// Name of the mapping.
    pub name: String,

    /// Address where the mapping will start when loaded on the
    /// console/emulator. This is the address where instructions like `jmp` or
    /// labels will rely on. Hence, it's not the address of the ROM file itself,
    /// but the effective address where it will be loaded.
    pub start: u16,

    /// Size of the mapping. Note that this depends on the `section_type` value,
    /// which is: exactly 0x10 for a header, multiples of 0x4000 for prg_rom,
    /// and multiples of 0x2000 for chr_rom.
    pub size: usize,

    /// The total number of bytes that have been pushed across all its segments.
    pub offset: usize,

    /// Optional value to fill the mapping if the end size is lower than the
    /// value on `size`. Set to `None` to skip filling the memory region for
    /// this mapping.
    pub fill: Option<u8>,

    /// Segments for the memory region.
    pub segments: Vec<Segment>,

    /// What kind of memory region is being described by this mapping.
    pub section_type: SectionType,
}

/// Returns a vector corresponding to the configuration of mappings that is
/// expected for the given `name`. This `name` can either be an already known
/// identifier (e.g. "nrom"), or a file path. Returns an error if the
/// configuration cannot be parsed or there's something wrong about it.
pub fn get_mapping_configuration(name: &str) -> Result<Vec<Mapping>, String> {
    let configuration = if std::fs::exists(name).unwrap_or(false) {
        match std::fs::read_to_string(name) {
            Ok(contents) => {
                // Call the right parse function.
                if contents.starts_with("#!nasmcfg") {
                    parse_nasm_cfg_file(contents.as_str())?
                } else {
                    parse_cfg_file(contents.as_str())?
                }
            }
            Err(_) => return Err(format!("could not read '{}'", name)),
        }
    } else {
        let text = match name.to_lowercase().as_str() {
            "empty" => EMPTY_CONFIG,
            "nrom" => NROM_CONFIG,
            "nrom65" => NROM65_CONFIG,
            "uxrom" | "unrom" => UXROM_CONFIG,
            _ => return Err("mapper configuration is not known".to_string()),
        };
        parse_nasm_cfg_file(text)?
    };

    validate_configuration(&configuration)?;

    Ok(configuration)
}

// Ensure that the given mappings conform to a minimum standard.
fn validate_configuration(mappings: &[Mapping]) -> Result<(), String> {
    if mappings.is_empty() {
        return Err("We need at least one segment defined, the header".to_string());
    }
    if mappings.first().unwrap().segments.is_empty() {
        return Err("We need at least one segment defined, the header".to_string());
    }
    if mappings.first().unwrap().section_type != SectionType::Header {
        return Err("First mapping section must be the header".to_string());
    }
    if mappings.first().unwrap().size != 0x10 {
        return Err("The header must be exactly 16 bytes long".to_string());
    }

    let prg_rom_len = mappings
        .iter()
        .filter(|m| m.section_type == SectionType::PrgRom)
        .fold(0, |acc, x| acc + x.size);

    if prg_rom_len < 0x4000 {
        return Err("PRG ROM must be at least 8KB long".to_string());
    }
    if prg_rom_len % 0x4000 != 0 {
        return Err("PRG ROM must be formed by banks of exactly 8KB".to_string());
    }

    Ok(())
}

/// Perform some sanity checks on the given `mappings`. Only call this function
/// after all bundles have been produced.
pub fn validate(mappings: &[Mapping]) -> Result<(), String> {
    // Guaranteed by `crate::mapping::assert` to be the header.
    let header: &Segment = mappings.first().unwrap().segments.first().unwrap();

    // Header must have at least six bytes with proper information provided by
    // the programmer.
    if header.len() < 6 {
        return Err(String::from("The header must contain at least 6 bytes"));
    }

    // Now check that the length of the evaluated data matches the criteria
    // stated on the ROM header that was evaluated as well.
    let (header_prg_rom_size, header_chr_rom_size) = parse_header(header)?;
    let prg_rom_len = mappings
        .iter()
        .filter(|m| m.section_type == SectionType::PrgRom)
        .fold(0, |acc, x| {
            acc + x.segments.iter().fold(0, |a, y| a + y.len())
        });
    let chr_rom_len = mappings
        .iter()
        .filter(|m| m.section_type == SectionType::ChrRom)
        .fold(0, |acc, x| {
            acc + x.segments.iter().fold(0, |a, y| a + y.len())
        });

    if header_prg_rom_size < prg_rom_len {
        return Err(format!(
            "PRG ROM size is expected to by {} bytes long, but a total of {} bytes were evaluated",
            header_prg_rom_size, prg_rom_len
        ));
    }
    if header_chr_rom_size < chr_rom_len {
        return Err(format!(
            "CHR ROM size is expected to by {} bytes long, but a total of {} bytes were evaluated",
            header_chr_rom_size, chr_rom_len
        ));
    }

    Ok(())
}

// Returns a tuple with the sizes for PRG and CHR ROM as described from the
// computed header. This also does some sanity checks on the header.
fn parse_header(header: &Segment) -> Result<(usize, usize), String> {
    let mut header_it = header.bundles.clone().into_iter();

    // Validate the magic string: 'N', 'E', 'S', $1A
    if header_it.next().unwrap().bytes[0] != b'N' {
        return Err(String::from("First byte of the header must be 'N'"));
    }
    if header_it.next().unwrap().bytes[0] != b'E' {
        return Err(String::from("Second byte of the header must be 'E'"));
    }
    if header_it.next().unwrap().bytes[0] != b'S' {
        return Err(String::from("Third byte of the header must be 'S'"));
    }
    if header_it.next().unwrap().bytes[0] != 26 {
        return Err(String::from(
            "Fourth byte of the header must be the MS-DOS termination character",
        ));
    }

    Ok((
        header_it.next().unwrap().bytes[0] as usize * 0x4000,
        header_it.next().unwrap().bytes[0] as usize * 0x2000,
    ))
}

/// Returns the offset of the segment indexed by `segment_index` inside of
/// `mapping`. That is, it returns back at which byte the given segment is going
/// to be placed inside of the given mapping.
///
/// NOTE: this function is only useful if you already know that all the segments
/// on the given mapping have a definitive size (i.e. they will not change in
/// the future).
pub fn segment_offset(mapping: &Mapping, segment_index: usize) -> u16 {
    let mut count = 0;

    for (idx, segment) in mapping.segments.iter().enumerate() {
        if idx >= segment_index {
            return count;
        }
        count += segment.offset as u16;
    }

    count
}