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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
//! Memory Initialization File
//!
//! # Features
//!
//!   * Native MIF representation as `Vec<(word: T, bulk: usize)>`.
//!   * Detects single-word sequence `[first..last]: word` but does **not**
//!     detect multi-word sequence `[first..last]: words..` in binary data.
//!   * Verifies word fits into MIF's word width in bits.
//!   * Joins multiple MIFs of different word widths as long as words fit.
//!   * Optionally comments join offsets in words with given (file) names.
//!   * Provides simple `mif dump` subcommand.
//!   * Provides reproducible `mif join` subcommand via TOML instruction file.
//!
//! # Library
//!
//! MIF creation and serialization is implemented for the `Mif` structure.
//!
//! Disable default features like `cli` and `bin` to reduce dependencies:
//!
//! ```toml
//! [dependencies]
//! mif = { version = "0.3", default-features = false }
//! ```
//!
//! Default features:
//!
//!   * `cli`: Provides command-line interface functionality of `mif` binary.
//!
//!     Requires: `anyhow`, `indexmap`, `serde`, `toml`
//!
//!   * `bin`: Enables compilation of `mif` binary.
//!
//!     Requires: `cli`, `clap`
//!
//! # Command-line Interface
//!
//! Install via `cargo install mif`.
//!
//! Provides two subcommands, `dump` and `join`.
//!
//! ```text
//! mif 0.3.0
//! Rouven Spreckels <rs@qu1x.dev>
//! Memory Initialization File
//!
//! USAGE:
//!     mif <SUBCOMMAND>
//!
//! OPTIONS:
//!     -h, --help       Prints help information
//!     -V, --version    Prints version information
//!
//! SUBCOMMANDS:
//!     dump    Dumps binary as MIF
//!     join    Joins binaries' memory areas to MIFs
//!     help    Prints this message or the help of the given subcommand(s)
//! ```
//!
//! ## Dump Subcommand
//!
//! ```text
//! mif-dump
//! Dumps binary as MIF
//!
//! USAGE:
//!     mif dump [input]
//!
//! ARGS:
//!     <input>    Input file or standard input (-) [default: -]
//!
//! OPTIONS:
//!     -w, --width <bits>       Word width in bits from 1 to 128 [default: 16]
//!     -f, --first <lsb|msb>    LSB/MSB first (little/big-endian) [default: lsb]
//!     -h, --help               Prints help information
//!     -V, --version            Prints version information
//! ```
//!
//! ## Join Subcommand
//!
//! ```text
//! mif-join
//! Joins binaries' memory areas to MIFs
//!
//! USAGE:
//!     mif join [OPTIONS] [toml]
//!
//! ARGS:
//!     <toml>    TOML file or standard input (-) [default: -]
//!
//! OPTIONS:
//!     -i, --bins <path>    Input directory [default: .]
//!     -o, --mifs <path>    Output directory [default: .]
//!     -n, --no-comments    No comments in MIFs
//!     -h, --help           Prints help information
//!     -V, --version        Prints version information
//! ```
//!
//! ### Join Example
//!
//! Assuming two ROM dumps, `a.rom` and `b.rom`, whose program and data areas
//! are concatenated as in:
//!
//!   * `cat a.program.rom a.data.rom > a.rom`
//!   * `cat b.program.rom b.data.rom > b.rom`
//!
//! Following TOML file defines how to join both program areas to one MIF and
//! both data areas to another MIF, assuming 24-bit program words of depth 1267
//! and 1747 and 16-bit data words of depth 1024 each. Additionally, every area
//! is dumped to its own separate MIF for verification. Then, between program
//! and data area is supposed to be an unused area of `0xffffff` words, which
//! should be skipped. Listing them in the `skips` instruction will verify that
//! this area only contains these words.
//!
//! ```toml
//! [["a.rom"]]
//! first = "lsb" # Least-significant byte first. Default, can be omitted.
//! width = 24
//! depth = 1267
//! joins = ["a.prog.mif", "ab.prog.mif"]
//! [["a.rom"]]
//! first = "lsb" # Least-significant byte first. Default, can be omitted.
//! width = 24
//! depth = 781
//! skips = [0xffffff] # Empty [] for skipping without verification.
//! [["a.rom"]]
//! first = "msb"
//! width = 16 # Default, can be omitted.
//! depth = 1024
//! joins = ["a.data.mif", "ab.data.mif"]
//!
//! [["b.rom"]]
//! width = 24
//! depth = 1747
//! joins = ["b.prog.mif", "ab.prog.mif"]
//! [["b.rom"]]
//! width = 24
//! depth = 301
//! skips = [0xffffff]
//! [["b.rom"]]
//! depth = 1024
//! joins = ["b.data.mif", "ab.data.mif"]
//! ```

#![forbid(unsafe_code)]
#![forbid(missing_docs)]

/// Command-line interface functionality of `mif` binary.
#[cfg(feature = "cli")]
pub mod cli;
#[cfg(feature = "cli")]
use serde::Deserialize;

use std::{
	mem::size_of,
	path::PathBuf,
	io::{self, Read, Write},
	result,
	fmt::UpperHex,
	str::FromStr,
};
use num_traits::{
	sign::Unsigned, int::PrimInt, cast::FromPrimitive,
	ops::{checked::CheckedShl, wrapping::WrappingSub},
};
use byteorder::{LE, BE, ReadBytesExt};
use thiserror::Error;
use First::{Lsb, Msb};
use Error::*;

type Result<T> = result::Result<T, Error>;

/// `Mif` errors.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
	/// Neither `"lsb"` nor `"msb"` first.
	#[error("Valid values are `lsb` and `msb`")]
	NeitherLsbNorMsbFirst,
	/// Width exceeds `[1, Mif::max_width()]`
	#[error("Width {0} out of [1, {1}]")]
	WidthOutOfRange(usize, usize),
	/// Word value exceeds `Mif::max_value()`.
	#[error("Word at depth {0} out of width {1}")]
	ValueOutOfWidth(usize, usize),
	/// Less words read than expected.
	#[error("Missing {0} words")]
	MissingWords(usize),
	/// I/O error.
	#[error(transparent)]
	IoError(#[from] io::Error),
}

/// Native MIF representation.
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Mif<T: UpperHex + Unsigned + PrimInt + FromPrimitive> {
	width: usize,
	depth: usize,
	words: Vec<(T, usize)>,
	areas: Vec<(usize, PathBuf)>,
}

impl<T> Mif<T>
where
	T: UpperHex + Unsigned + PrimInt + FromPrimitive + CheckedShl + WrappingSub,
{
	/// Creates new MIF with word `width`.
	pub fn new(width: usize) -> Result<Mif<T>> {
		if (1..=Self::max_width()).contains(&width) {
			Ok(Mif { words: Vec::new(), depth: 0, areas: Vec::new(), width })
		} else {
			Err(WidthOutOfRange(width, Self::max_width()))
		}
	}
	/// Maximum word width in bits depending on `T`.
	pub fn max_width() -> usize {
		Self::max_align() * 8
	}
	/// Maximum word width in bytes depending on `T`.
	pub fn max_align() -> usize {
		size_of::<T>()
	}
	/// Maximum word value depending on `width()`.
	pub fn max_value(&self) -> T {
		T::one().checked_shl(self.width as u32)
			.unwrap_or(T::zero()).wrapping_sub(&T::one())
	}
	/// Word width in bits.
	pub fn width(&self) -> usize {
		self.width
	}
	/// Word width in bytes.
	pub fn align(&self) -> usize {
		(self.width as f64 / 8.0).ceil() as usize
	}
	/// MIF depth in words.
	pub fn depth(&self) -> usize {
		self.depth
	}
	/// Reference to words and their bulk in given order.
	pub fn words(&self) -> &Vec<(T, usize)> {
		&self.words
	}
	/// Reference to addresses and paths of memory areas in given order.
	pub fn areas(&self) -> &Vec<(usize, PathBuf)> {
		&self.areas
	}
	/// Addresses memory `area` at current `depth()`.
	pub fn area(&mut self, area: PathBuf) {
		self.areas.push((self.depth, area));
	}
	/// Pushes `word` or add up its `bulk`.
	pub fn push(&mut self, word: T, bulk: usize) -> Result<()> {
		match self.words.last_mut() {
			Some((last_word, last_bulk)) if *last_word == word =>
				*last_bulk += bulk,
			_ => {
				if word > self.max_value() {
					Err(ValueOutOfWidth(self.depth, self.width()))?;
				}
				if bulk > 0 {
					self.words.push((word, bulk))
				}
			},
		}
		self.depth += bulk;
		Ok(())
	}
	/// Joins in `other` MIF.
	pub fn join(&mut self, other: &Self) -> Result<()> {
		other.words.iter().try_for_each(|&(word, bulk)| self.push(word, bulk))
	}
	/// Reads `depth` LSB/MSB-`first` words from `bytes` reader.
	pub fn read(&mut self, bytes: &mut dyn Read, depth: usize, first: First)
	-> Result<()> {
		let align = self.align();
		let mut words = 0;
		for _ in 0..depth {
			let word = match first {
				Lsb => bytes.read_uint128::<LE>(align),
				Msb => bytes.read_uint128::<BE>(align),
			}?;
			self.push(T::from_u128(word)
				.ok_or(ValueOutOfWidth(words, self.width))?, 1)?;
			words += 1;
		}
		if depth != words {
			Err(MissingWords(depth - words))?;
		}
		Ok(())
	}
	/// Writes MIF to writer.
	///
	///   * `lines`: Writer, MIF is written to.
	///   * `areas`: Whether to comment memory areas as in `-- 0000: name.bin`.
	pub fn write(&self, lines: &mut dyn Write, areas: bool) -> Result<()> {
		let addr_pads = (self.depth as f64).log(16.0).ceil() as usize;
		let word_pads = (self.width as f64 / 4.0).ceil() as usize;
		if areas && !self.areas.is_empty() {
			for (addr, path) in &self.areas {
				writeln!(lines, "-- {:02$X}: {}",
					addr, path.display(), addr_pads)?;
			}
			writeln!(lines)?;
		}
		writeln!(lines, "\
			WIDTH={};\n\
			DEPTH={};\n\
			\n\
			ADDRESS_RADIX=HEX;\n\
			DATA_RADIX=HEX;\n\
			\n\
			CONTENT BEGIN", self.width, self.depth)?;
		let mut addr = 0;
		for &(word, bulk) in &self.words {
			if bulk == 1 {
				writeln!(lines, "\t{:02$X}  :   {:03$X};",
					addr, word, addr_pads, word_pads)?;
			} else {
				writeln!(lines, "\t[{:03$X}..{:03$X}]  :   {:04$X};",
					addr, addr + bulk - 1, word, addr_pads, word_pads)?;
			}
			addr += bulk;
		}
		writeln!(lines, "END;")?;
		Ok(())
	}
}

/// LSB/MSB first (little/big-endian).
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
#[cfg_attr(feature = "cli", derive(Deserialize))]
#[cfg_attr(feature = "cli", serde(rename_all = "kebab-case"))]
pub enum First {
	/// Least-significant byte first (little-endian).
	Lsb,
	/// Most-significant byte first (big-endian).
	Msb,
}

impl Default for First {
	fn default() -> Self { Lsb }
}

impl FromStr for First {
	type Err = Error;

	fn from_str(from: &str) -> Result<Self> {
		match from {
			"lsb" => Ok(Lsb),
			"msb" => Ok(Msb),
			_ => Err(NeitherLsbNorMsbFirst),
		}
	}
}

/// Default width of 16 bits.
pub const fn default_width() -> usize { 16 }