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 }