mif/lib.rs
1//! Memory Initialization File
2//!
3//! # Features
4//!
5//! * Native MIF representation as `Vec<(word: T, bulk: usize)>`.
6//! * Detects single-word sequence `[first..last]: word` but does **not**
7//! detect multi-word sequence `[first..last]: words..` in binary data.
8//! * Verifies word fits into MIF's word width in bits.
9//! * Joins multiple MIFs of different word widths as long as words fit.
10//! * Optionally comments join offsets in words with given (file) names.
11//! * Provides simple `mif dump` subcommand.
12//! * Provides reproducible `mif join` subcommand via TOML instruction file.
13//!
14//! # Library
15//!
16//! MIF creation and serialization is implemented for the `Mif` structure.
17//!
18//! Disable default features like `cli` and `bin` to reduce dependencies:
19//!
20//! ```toml
21//! [dependencies]
22//! mif = { version = "0.3", default-features = false }
23//! ```
24//!
25//! Default features:
26//!
27//! * `cli`: Provides command-line interface functionality of `mif` binary.
28//!
29//! Requires: `anyhow`, `indexmap`, `serde`, `toml`
30//!
31//! * `bin`: Enables compilation of `mif` binary.
32//!
33//! Requires: `cli`, `clap`
34//!
35//! # Command-line Interface
36//!
37//! Install via `cargo install mif`.
38//!
39//! Provides two subcommands, `dump` and `join`.
40//!
41//! ```text
42//! mif 0.3.0
43//! Rouven Spreckels <rs@qu1x.dev>
44//! Memory Initialization File
45//!
46//! USAGE:
47//! mif <SUBCOMMAND>
48//!
49//! OPTIONS:
50//! -h, --help Prints help information
51//! -V, --version Prints version information
52//!
53//! SUBCOMMANDS:
54//! dump Dumps binary as MIF
55//! join Joins binaries' memory areas to MIFs
56//! help Prints this message or the help of the given subcommand(s)
57//! ```
58//!
59//! ## Dump Subcommand
60//!
61//! ```text
62//! mif-dump
63//! Dumps binary as MIF
64//!
65//! USAGE:
66//! mif dump [input]
67//!
68//! ARGS:
69//! <input> Input file or standard input (-) [default: -]
70//!
71//! OPTIONS:
72//! -w, --width <bits> Word width in bits from 1 to 128 [default: 16]
73//! -f, --first <lsb|msb> LSB/MSB first (little/big-endian) [default: lsb]
74//! -h, --help Prints help information
75//! -V, --version Prints version information
76//! ```
77//!
78//! ## Join Subcommand
79//!
80//! ```text
81//! mif-join
82//! Joins binaries' memory areas to MIFs
83//!
84//! USAGE:
85//! mif join [OPTIONS] [toml]
86//!
87//! ARGS:
88//! <toml> TOML file or standard input (-) [default: -]
89//!
90//! OPTIONS:
91//! -i, --bins <path> Input directory [default: .]
92//! -o, --mifs <path> Output directory [default: .]
93//! -n, --no-comments No comments in MIFs
94//! -h, --help Prints help information
95//! -V, --version Prints version information
96//! ```
97//!
98//! ### Join Example
99//!
100//! Assuming two ROM dumps, `a.rom` and `b.rom`, whose program and data areas
101//! are concatenated as in:
102//!
103//! * `cat a.program.rom a.data.rom > a.rom`
104//! * `cat b.program.rom b.data.rom > b.rom`
105//!
106//! Following TOML file defines how to join both program areas to one MIF and
107//! both data areas to another MIF, assuming 24-bit program words of depth 1267
108//! and 1747 and 16-bit data words of depth 1024 each. Additionally, every area
109//! is dumped to its own separate MIF for verification. Then, between program
110//! and data area is supposed to be an unused area of `0xffffff` words, which
111//! should be skipped. Listing them in the `skips` instruction will verify that
112//! this area only contains these words.
113//!
114//! ```toml
115//! [["a.rom"]]
116//! first = "lsb" # Least-significant byte first. Default, can be omitted.
117//! width = 24
118//! depth = 1267
119//! joins = ["a.prog.mif", "ab.prog.mif"]
120//! [["a.rom"]]
121//! first = "lsb" # Least-significant byte first. Default, can be omitted.
122//! width = 24
123//! depth = 781
124//! skips = [0xffffff] # Empty [] for skipping without verification.
125//! [["a.rom"]]
126//! first = "msb"
127//! width = 16 # Default, can be omitted.
128//! depth = 1024
129//! joins = ["a.data.mif", "ab.data.mif"]
130//!
131//! [["b.rom"]]
132//! width = 24
133//! depth = 1747
134//! joins = ["b.prog.mif", "ab.prog.mif"]
135//! [["b.rom"]]
136//! width = 24
137//! depth = 301
138//! skips = [0xffffff]
139//! [["b.rom"]]
140//! depth = 1024
141//! joins = ["b.data.mif", "ab.data.mif"]
142//! ```
143
144#![forbid(unsafe_code)]
145#![forbid(missing_docs)]
146
147/// Command-line interface functionality of `mif` binary.
148#[cfg(feature = "cli")]
149pub mod cli;
150#[cfg(feature = "cli")]
151use serde::Deserialize;
152
153use std::{
154 mem::size_of,
155 path::PathBuf,
156 io::{self, Read, Write},
157 result,
158 fmt::UpperHex,
159 str::FromStr,
160};
161use num_traits::{
162 sign::Unsigned, int::PrimInt, cast::FromPrimitive,
163 ops::{checked::CheckedShl, wrapping::WrappingSub},
164};
165use byteorder::{LE, BE, ReadBytesExt};
166use thiserror::Error;
167use First::{Lsb, Msb};
168use Error::*;
169
170type Result<T> = result::Result<T, Error>;
171
172/// `Mif` errors.
173#[derive(Error, Debug)]
174#[non_exhaustive]
175pub enum Error {
176 /// Neither `"lsb"` nor `"msb"` first.
177 #[error("Valid values are `lsb` and `msb`")]
178 NeitherLsbNorMsbFirst,
179 /// Width exceeds `[1, Mif::max_width()]`
180 #[error("Width {0} out of [1, {1}]")]
181 WidthOutOfRange(usize, usize),
182 /// Word value exceeds `Mif::max_value()`.
183 #[error("Word at depth {0} out of width {1}")]
184 ValueOutOfWidth(usize, usize),
185 /// Less words read than expected.
186 #[error("Missing {0} words")]
187 MissingWords(usize),
188 /// I/O error.
189 #[error(transparent)]
190 IoError(#[from] io::Error),
191}
192
193/// Native MIF representation.
194#[derive(Debug, Eq, PartialEq, Clone)]
195pub struct Mif<T: UpperHex + Unsigned + PrimInt + FromPrimitive> {
196 width: usize,
197 depth: usize,
198 words: Vec<(T, usize)>,
199 areas: Vec<(usize, PathBuf)>,
200}
201
202impl<T> Mif<T>
203where
204 T: UpperHex + Unsigned + PrimInt + FromPrimitive + CheckedShl + WrappingSub,
205{
206 /// Creates new MIF with word `width`.
207 pub fn new(width: usize) -> Result<Mif<T>> {
208 if (1..=Self::max_width()).contains(&width) {
209 Ok(Mif { words: Vec::new(), depth: 0, areas: Vec::new(), width })
210 } else {
211 Err(WidthOutOfRange(width, Self::max_width()))
212 }
213 }
214 /// Maximum word width in bits depending on `T`.
215 pub fn max_width() -> usize {
216 Self::max_align() * 8
217 }
218 /// Maximum word width in bytes depending on `T`.
219 pub fn max_align() -> usize {
220 size_of::<T>()
221 }
222 /// Maximum word value depending on `width()`.
223 pub fn max_value(&self) -> T {
224 T::one().checked_shl(self.width as u32)
225 .unwrap_or(T::zero()).wrapping_sub(&T::one())
226 }
227 /// Word width in bits.
228 pub fn width(&self) -> usize {
229 self.width
230 }
231 /// Word width in bytes.
232 pub fn align(&self) -> usize {
233 (self.width as f64 / 8.0).ceil() as usize
234 }
235 /// MIF depth in words.
236 pub fn depth(&self) -> usize {
237 self.depth
238 }
239 /// Reference to words and their bulk in given order.
240 pub fn words(&self) -> &Vec<(T, usize)> {
241 &self.words
242 }
243 /// Reference to addresses and paths of memory areas in given order.
244 pub fn areas(&self) -> &Vec<(usize, PathBuf)> {
245 &self.areas
246 }
247 /// Addresses memory `area` at current `depth()`.
248 pub fn area(&mut self, area: PathBuf) {
249 self.areas.push((self.depth, area));
250 }
251 /// Pushes `word` or add up its `bulk`.
252 pub fn push(&mut self, word: T, bulk: usize) -> Result<()> {
253 match self.words.last_mut() {
254 Some((last_word, last_bulk)) if *last_word == word =>
255 *last_bulk += bulk,
256 _ => {
257 if word > self.max_value() {
258 Err(ValueOutOfWidth(self.depth, self.width()))?;
259 }
260 if bulk > 0 {
261 self.words.push((word, bulk))
262 }
263 },
264 }
265 self.depth += bulk;
266 Ok(())
267 }
268 /// Joins in `other` MIF.
269 pub fn join(&mut self, other: &Self) -> Result<()> {
270 other.words.iter().try_for_each(|&(word, bulk)| self.push(word, bulk))
271 }
272 /// Reads `depth` LSB/MSB-`first` words from `bytes` reader.
273 pub fn read(&mut self, bytes: &mut dyn Read, depth: usize, first: First)
274 -> Result<()> {
275 let align = self.align();
276 let mut words = 0;
277 for _ in 0..depth {
278 let word = match first {
279 Lsb => bytes.read_uint128::<LE>(align),
280 Msb => bytes.read_uint128::<BE>(align),
281 }?;
282 self.push(T::from_u128(word)
283 .ok_or(ValueOutOfWidth(words, self.width))?, 1)?;
284 words += 1;
285 }
286 if depth != words {
287 Err(MissingWords(depth - words))?;
288 }
289 Ok(())
290 }
291 /// Writes MIF to writer.
292 ///
293 /// * `lines`: Writer, MIF is written to.
294 /// * `areas`: Whether to comment memory areas as in `-- 0000: name.bin`.
295 pub fn write(&self, lines: &mut dyn Write, areas: bool) -> Result<()> {
296 let addr_pads = (self.depth as f64).log(16.0).ceil() as usize;
297 let word_pads = (self.width as f64 / 4.0).ceil() as usize;
298 if areas && !self.areas.is_empty() {
299 for (addr, path) in &self.areas {
300 writeln!(lines, "-- {:02$X}: {}",
301 addr, path.display(), addr_pads)?;
302 }
303 writeln!(lines)?;
304 }
305 writeln!(lines, "\
306 WIDTH={};\n\
307 DEPTH={};\n\
308 \n\
309 ADDRESS_RADIX=HEX;\n\
310 DATA_RADIX=HEX;\n\
311 \n\
312 CONTENT BEGIN", self.width, self.depth)?;
313 let mut addr = 0;
314 for &(word, bulk) in &self.words {
315 if bulk == 1 {
316 writeln!(lines, "\t{:02$X} : {:03$X};",
317 addr, word, addr_pads, word_pads)?;
318 } else {
319 writeln!(lines, "\t[{:03$X}..{:03$X}] : {:04$X};",
320 addr, addr + bulk - 1, word, addr_pads, word_pads)?;
321 }
322 addr += bulk;
323 }
324 writeln!(lines, "END;")?;
325 Ok(())
326 }
327}
328
329/// LSB/MSB first (little/big-endian).
330#[derive(Debug, Eq, PartialEq, Copy, Clone)]
331#[cfg_attr(feature = "cli", derive(Deserialize))]
332#[cfg_attr(feature = "cli", serde(rename_all = "kebab-case"))]
333pub enum First {
334 /// Least-significant byte first (little-endian).
335 Lsb,
336 /// Most-significant byte first (big-endian).
337 Msb,
338}
339
340impl Default for First {
341 fn default() -> Self { Lsb }
342}
343
344impl FromStr for First {
345 type Err = Error;
346
347 fn from_str(from: &str) -> Result<Self> {
348 match from {
349 "lsb" => Ok(Lsb),
350 "msb" => Ok(Msb),
351 _ => Err(NeitherLsbNorMsbFirst),
352 }
353 }
354}
355
356/// Default width of 16 bits.
357pub const fn default_width() -> usize { 16 }