From 231892b5b33b6ef92c674cd8b6565f07ff1f3c6a Mon Sep 17 00:00:00 2001 From: Rouven Spreckels Date: Wed, 12 Aug 2020 14:19:22 +0200 Subject: [PATCH] Refactor, add `cli` feature. --- .travis.yml | 8 +- Cargo.toml | 17 +-- README.md | 16 ++- RELEASES.md | 5 + src/{main.rs => bin/mif.rs} | 6 +- src/cli.rs | 180 +++++++++++++++++++++++++++++++ src/lib.rs | 210 +++++------------------------------- 7 files changed, 245 insertions(+), 197 deletions(-) rename src/{main.rs => bin/mif.rs} (95%) create mode 100644 src/cli.rs diff --git a/.travis.yml b/.travis.yml index d619cbc..55fbd84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,17 @@ matrix: - rust: stable - rust: stable env: FEATURES="cli" + - rust: stable + env: FEATURES="bin" - rust: beta - rust: beta env: FEATURES="cli" + - rust: beta + env: FEATURES="bin" - rust: nightly - rust: nightly env: FEATURES="cli" + - rust: nightly + env: FEATURES="bin" script: - - cargo test --verbose --no-default-features --features "$FEATURES" + - cargo test --verbose --no-default-features --features "$FEATURES" diff --git a/Cargo.toml b/Cargo.toml index 74b5a26..03a5439 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mif" -version = "0.1.1" +version = "0.2.0" authors = ["Rouven Spreckels "] edition = "2018" description = "Memory Initialization File" @@ -34,18 +34,19 @@ path = "src/lib.rs" [[bin]] name = "mif" -path = "src/main.rs" -required-features = ["cli"] +path = "src/bin/mif.rs" +required-features = ["bin"] [dependencies] -serde = { version = "1", features = ["derive"] } -indexmap = { version = "1", features = ["serde-1"] } num-traits = "0.2" byteorder = "1" anyhow = "1" -clap = { version = "3.0.0-beta.1", optional = true } +indexmap = { version = "1", features = ["serde-1"], optional = true } +serde = { version = "1", features = ["derive"], optional = true } toml = { version = "0.5", features = ["preserve_order"], optional = true } +clap = { version = "3.0.0-beta.1", optional = true } [features] -default = ["cli"] -cli = ["clap", "toml"] +default = ["bin"] +cli = ["indexmap", "serde", "toml"] +bin = ["cli", "clap"] diff --git a/README.md b/README.md index 45c7337..c649002 100644 --- a/README.md +++ b/README.md @@ -20,19 +20,29 @@ Memory Initialization File MIF creation and serialization is implemented via the `Mif` structure. -Disable default features like `cli` to reduce dependencies: +Disable default features like `cli` and `bin` to reduce dependencies: ```toml [dependencies] -mif = { version = "0.1", default-features = false } +mif = { version = "0.2", default-features = false } ``` +Default features: + + * `cli`: Provides command-line interface functionality of `mif` binary. + + Requires: `indexmap`, `serde`, `toml` + + * `bin`: Enables compilation of `mif` binary. + + Requires: `cli`, `clap` + ## Command-line Interface Provides two subcommands, `dump` and `join`. ```text -mif 0.1.1 +mif 0.2.0 Rouven Spreckels Memory Initialization File diff --git a/RELEASES.md b/RELEASES.md index 813d299..2758ae6 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,8 @@ +# Version 0.2.0 (2020-08-12) + + * Refactor, add `cli` feature. + * Fix MIF output directory. + # Version 0.1.1 (2020-08-11) * Fix optional dependencies. diff --git a/src/main.rs b/src/bin/mif.rs similarity index 95% rename from src/main.rs rename to src/bin/mif.rs index 0a9715e..bf9cdd5 100644 --- a/src/main.rs +++ b/src/bin/mif.rs @@ -1,4 +1,4 @@ -//! Memory Initialization File +//! `mif` binary. #![forbid(unsafe_code)] #![forbid(missing_docs)] @@ -6,8 +6,8 @@ use std::{path::PathBuf, io::stdout}; use clap::{crate_version, crate_authors, Clap, AppSettings}; use anyhow::Result; -use mif::{First, open, dump, load, join}; -use Mif::*; +use mif::{First, cli::{open, dump, load, join}}; +use Mif::{Dump, Join}; /// Memory Initialization File. #[derive(Clap)] diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..d08e746 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,180 @@ +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +use std::{ + convert::TryInto, + path::{PathBuf, Path}, + fs::{OpenOptions, metadata}, + io::{Cursor, BufReader, Read, stdin, BufWriter, Write}, +}; +use serde::Deserialize; +use indexmap::IndexMap; +use anyhow::{Result, Context, ensure}; +use Instr::{Skips, Joins}; +use crate::{Mif, First, default_width}; + +/// Opens file or standard input `"-"` as buffered bytes reader of known count. +/// +/// * For a file, the count is determined by `metadata()`. +/// * For standard input, the bytes are completely read in and counted. +pub fn open(input: &dyn AsRef) -> Result<(Box, usize)> { + let input = input.as_ref(); + Ok(if input == Path::new("-") { + let mut bytes = Vec::new(); + stdin().read_to_end(&mut bytes).context("Cannot read standard input")?; + let count = bytes.len(); + (Box::new(Cursor::new(bytes)), count) + } else { + let (bytes, count) = OpenOptions::new().read(true).open(&input) + .and_then(|bytes| metadata(&input) + .map(|stats| (BufReader::new(bytes), stats.len()))) + .with_context(|| format!("Cannot open `{}`", input.display()))?; + (Box::new(bytes), count.try_into().context("Address space exhausted")?) + }) +} + +/// Dumps known count of bytes from reader as MIF to writer. +/// +/// * `lines`: Writer, MIF is written to. +/// * `bytes`: Reader, bytes are read from. +/// * `count`: Count of bytes to read. +/// * `width`: Word width in bits from 8 to 128. +/// * `first`: LSB/MSB first (little/big-endian). +pub fn dump( + lines: &mut dyn Write, + bytes: &mut dyn Read, + count: usize, + width: usize, + first: First, +) -> Result<()> { + let mut mif = Mif::::new(width)?; + let align = mif.align(); + let depth = count / align; + ensure!(depth * align == count, "No integral multiple of word width"); + mif.read(bytes, depth, first).context("Cannot read input") + .and_then(|()| mif.write(lines, false).context("Cannot write MIF")) +} + +/// Load TOML from file or standard input `"-"` as `Files`. +pub fn load(input: &dyn AsRef) -> Result { + let input = input.as_ref(); + let mut file: Box = if input == Path::new("-") { + Box::new(stdin()) + } else { + Box::new(OpenOptions::new().read(true).open(&input).map(BufReader::new) + .with_context(|| format!("Cannot open `{}`", input.display()))?) + }; + let mut string = String::new(); + file.read_to_string(&mut string) + .with_context(|| format!("Cannot read `{}`", input.display())) + .and_then(|_count| toml::from_str::(&string) + .with_context(|| format!("Cannot load `{}`", input.display()))) +} + +/// Joins memory areas of binary `Files` as MIFs. +/// +/// * `files`: Binary files split into memory areas, see `Files`. +/// * `areas`: Whether to comment memory areas, see `write()`. +/// * `paths`: Prefix paths for input binaries and output MIFs in given order. +pub fn join( + files: &Files, + paths: (&dyn AsRef, &dyn AsRef), + areas: bool, +) -> Result<()> { + let mut mifs = IndexMap::new(); + for (bin_path, areas) in files { + let mut abs_path = paths.0.as_ref().to_path_buf(); + abs_path.push(&bin_path); + let mut bin_file = OpenOptions::new() + .read(true).open(&abs_path).map(BufReader::new) + .with_context(|| format!("Cannot open `{}`", abs_path.display()))?; + for &Area { first, width, depth, ref instr } in areas { + let mut mif_area = Mif::new(width)?; + mif_area.read(&mut bin_file, depth, first)?; + match instr { + Skips(skips) => if !skips.is_empty() { + ensure!(mif_area.words().iter() + .all(|&(word, _bulk)| skips.iter() + .any(|skip| skip.as_word() == word)), + "Invalid word to skip in `{}`", bin_path.display()); + }, + Joins(joins) => for mif_path in joins { + if !mifs.contains_key(mif_path) { + let mut abs_path = paths.1.as_ref().to_path_buf(); + abs_path.push(mif_path); + let mif_file = OpenOptions::new() + .write(true).create(true).truncate(true) + .open(&abs_path).map(BufWriter::new) + .with_context(|| format!("Cannot open `{}`", + abs_path.display()))?; + let mif = (mif_file, Mif::new(width)?); + assert!(mifs.insert(mif_path.clone(), mif).is_none()); + } + let (_mif_file, mif_data) = &mut mifs[mif_path]; + ensure!(mif_data.width() == width, + "Different width to join `{}`", mif_path.display()); + mif_data.area(bin_path.clone()); + mif_data.join(&mif_area)?; + }, + } + } + let mut bin_data = Vec::new(); + bin_file.read_to_end(&mut bin_data)?; + ensure!(bin_data.is_empty(), + "{} B left over in `{}`", bin_data.len(), bin_path.display()); + } + for (mif_path, (mut mif_file, mif_data)) in mifs { + mif_data.write(&mut mif_file, areas) + .with_context(|| format!("Cannot write `{}`", mif_path.display()))?; + } + Ok(()) +} + +/// Binary files split into memory areas. +pub type Files = IndexMap>; + +/// Memory area. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Area { + /// LSB/MSB first (little/big-endian). + #[serde(default)] + pub first: First, + /// Word width in bits from 8 to 128. + #[serde(default = "default_width")] + pub width: usize, + /// Depth in words. + pub depth: usize, + /// Whether to skip or join this memory area. + #[serde(flatten)] + pub instr: Instr, +} + +/// Whether to skip or join a memory area. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Instr { + /// Skips memory area and ensures it contains given words only. + Skips(Vec), + /// Joins memory area to given MIFs. + Joins(Vec), +} + +/// TOML `u128` workaround. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)] +#[serde(untagged)] +pub enum Word { + /// One `u64` TOML integer as `u128`. + One(u64), + /// Two `u64` TOML integers `[msb, lsb]` as `u128`. + Two([u64; 2]) +} + +impl Word { + fn as_word(&self) -> u128 { + match *self { + Word::One(one) => one as u128, + Word::Two(two) => (two[0] as u128) << 64 | two[1] as u128, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 6baf8cf..2fdb814 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,19 +4,29 @@ //! //! MIF creation and serialization is implemented via the `Mif` structure. //! -//! Disable default features like `cli` to reduce dependencies: +//! Disable default features like `cli` and `bin` to reduce dependencies: //! //! ```toml //! [dependencies] -//! mif = { version = "0.1", default-features = false } +//! mif = { version = "0.2", default-features = false } //! ``` //! +//! Default features: +//! +//! * `cli`: Provides command-line interface functionality of `mif` binary. +//! +//! Requires: `indexmap`, `serde`, `toml` +//! +//! * `bin`: Enables compilation of `mif` binary. +//! +//! Requires: `cli`, `clap` +//! //! # Command-line Interface //! //! Provides two subcommands, `dump` and `join`. //! //! ```text -//! mif 0.1.1 +//! mif 0.2.0 //! Rouven Spreckels //! Memory Initialization File //! @@ -121,139 +131,22 @@ #![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::{ - convert::TryInto, - path::{PathBuf, Path}, - fs::{OpenOptions, metadata}, - io::{Cursor, BufReader, Read, stdin, BufWriter, Write}, + path::PathBuf, + io::{Read, Write}, fmt::UpperHex, str::FromStr, }; -use serde::Deserialize; -use indexmap::IndexMap; use num_traits::{int::PrimInt, cast::FromPrimitive}; use byteorder::{LE, BE, ReadBytesExt}; -use anyhow::{Error, Result, Context, anyhow, ensure}; -use First::*; -use Instr::*; - -/// Opens file or standard input `"-"` as buffered bytes reader of known count. -/// -/// * For a file, the count is determined by `metadata()`. -/// * For standard input, the bytes are completely read in and counted. -pub fn open(input: &dyn AsRef) -> Result<(Box, usize)> { - let input = input.as_ref(); - Ok(if input == Path::new("-") { - let mut bytes = Vec::new(); - stdin().read_to_end(&mut bytes).context("Cannot read standard input")?; - let count = bytes.len(); - (Box::new(Cursor::new(bytes)), count) - } else { - let (bytes, count) = OpenOptions::new().read(true).open(&input) - .and_then(|bytes| metadata(&input) - .map(|stats| (BufReader::new(bytes), stats.len()))) - .with_context(|| format!("Cannot open `{}`", input.display()))?; - (Box::new(bytes), count.try_into().context("Address space exhausted")?) - }) -} - -/// Dumps known count of bytes from reader as MIF to writer. -/// -/// * `lines`: Writer, MIF is written to. -/// * `bytes`: Reader, bytes are read from. -/// * `count`: Count of bytes to read. -/// * `width`: Word width in bits from 8 to 128. -/// * `first`: LSB/MSB first (little/big-endian). -pub fn dump( - lines: &mut dyn Write, - bytes: &mut dyn Read, - count: usize, - width: usize, - first: First, -) -> Result<()> { - let mut mif = Mif::::new(width)?; - let align = mif.align(); - let depth = count / align; - ensure!(depth * align == count, "No integral multiple of word width"); - mif.read(bytes, depth, first).context("Cannot read input") - .and_then(|()| mif.write(lines, false).context("Cannot write MIF")) -} - -/// Load TOML from file or standard input `"-"` as `Files`. -#[cfg(feature = "cli")] -pub fn load(input: &dyn AsRef) -> Result { - let input = input.as_ref(); - let mut file: Box = if input == Path::new("-") { - Box::new(stdin()) - } else { - Box::new(OpenOptions::new().read(true).open(&input).map(BufReader::new) - .with_context(|| format!("Cannot open `{}`", input.display()))?) - }; - let mut string = String::new(); - file.read_to_string(&mut string) - .with_context(|| format!("Cannot read `{}`", input.display())) - .and_then(|_count| toml::from_str::(&string) - .with_context(|| format!("Cannot load `{}`", input.display()))) -} - -/// Joins memory areas of binary `Files` as MIFs. -/// -/// * `files`: Binary files split into memory areas, see `Files`. -/// * `areas`: Whether to comment memory areas, see `write()`. -/// * `paths`: Prefix paths for input binaries and output MIFs in given order. -pub fn join( - files: &Files, - paths: (&dyn AsRef, &dyn AsRef), - areas: bool, -) -> Result<()> { - let mut mifs = IndexMap::new(); - for (bin_path, areas) in files { - let mut abs_path = paths.0.as_ref().to_path_buf(); - abs_path.push(&bin_path); - let mut bin_file = OpenOptions::new() - .read(true).open(&abs_path).map(BufReader::new) - .with_context(|| format!("Cannot open `{}`", abs_path.display()))?; - for &Area { first, width, depth, ref instr } in areas { - let mut mif_area = Mif::new(width)?; - mif_area.read(&mut bin_file, depth, first)?; - match instr { - Skips(skips) => if !skips.is_empty() { - ensure!(mif_area.words().iter() - .all(|&(word, _bulk)| skips.iter() - .any(|skip| skip.as_word() == word)), - "Invalid word to skip in `{}`", bin_path.display()); - }, - Joins(joins) => for mif_path in joins { - if !mifs.contains_key(mif_path) { - let mut abs_path = paths.1.as_ref().to_path_buf(); - abs_path.push(mif_path); - let mif_file = OpenOptions::new() - .write(true).create(true).truncate(true) - .open(&abs_path).map(BufWriter::new) - .with_context(|| format!("Cannot open `{}`", - abs_path.display()))?; - let mif = (mif_file, Mif::new(width)?); - assert!(mifs.insert(mif_path.clone(), mif).is_none()); - } - let (_mif_file, mif_data) = &mut mifs[mif_path]; - ensure!(mif_data.width() == width, - "Different width to join `{}`", mif_path.display()); - mif_data.area(bin_path.clone()); - mif_data.join(&mif_area)?; - }, - } - } - let mut bin_data = Vec::new(); - bin_file.read_to_end(&mut bin_data)?; - ensure!(bin_data.is_empty(), - "{} B left over in `{}`", bin_data.len(), bin_path.display()); - } - for (mif_path, (mut mif_file, mif_data)) in mifs { - mif_data.write(&mut mif_file, areas) - .with_context(|| format!("Cannot write `{}`", mif_path.display()))?; - } - Ok(()) -} +use anyhow::{Error, Result, anyhow, ensure}; +use First::{Lsb, Msb}; /// Native MIF representation. #[derive(Debug, Eq, PartialEq, Clone)] @@ -370,31 +263,10 @@ impl Mif { } } -/// Binary files split into memory areas. -pub type Files = IndexMap>; - -/// Memory area. -#[derive(Debug, Eq, PartialEq, Clone, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Area { - /// LSB/MSB first (little/big-endian). - #[serde(default)] - pub first: First, - /// Word width in bits from 8 to 128. - #[serde(default = "default_width")] - pub width: usize, - /// Depth in words. - pub depth: usize, - /// Whether to skip or join this memory area. - #[serde(flatten)] - pub instr: Instr, -} - -const fn default_width() -> usize { 16 } - /// LSB/MSB first (little/big-endian). -#[derive(Debug, Eq, PartialEq, Copy,Clone, Deserialize)] -#[serde(rename_all = "kebab-case")] +#[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, @@ -418,31 +290,5 @@ impl FromStr for First { } } -/// Whether to skip or join a memory area. -#[derive(Debug, Eq, PartialEq, Clone, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum Instr { - /// Skips memory area and ensures it contains given words only. - Skips(Vec), - /// Joins memory area to given MIFs. - Joins(Vec), -} - -/// TOML `u128` workaround. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)] -#[serde(untagged)] -pub enum Word { - /// One `u64` TOML integer as `u128`. - One(u64), - /// Two `u64` TOML integers `[msb, lsb]` as `u128`. - Two([u64; 2]) -} - -impl Word { - fn as_word(&self) -> u128 { - match *self { - Word::One(one) => one as u128, - Word::Two(two) => (two[0] as u128) << 64 | two[1] as u128, - } - } -} +/// Default width of 16 bits. +pub const fn default_width() -> usize { 16 }