use std::borrow::Cow;
use std::fs::File;
use std::io::{Read, Write};
#[cfg(target_os = "linux")]
use std::os::linux::fs::MetadataExt;
#[cfg(target_os = "macos")]
use std::os::macos::fs::MetadataExt;
#[cfg(target_os = "windows")]
use std::os::windows::fs::MetadataExt;
use std::path::{Path, PathBuf};
use svd_parser::svd::{BitRange, Field};
use anyhow::{anyhow, Context};
use liquid::{
model::{object, Scalar},
Object,
};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use svd_parser::expand::{
derive_cluster, derive_enumerated_values, derive_register, BlockPath, RegisterPath,
};
use svd_parser::{
expand::{derive_peripheral, Index},
svd::{Access, Cluster, Register, RegisterInfo, WriteConstraint},
};
fn sanitize(input: &str) -> String {
use once_cell::sync::Lazy;
use regex::Regex;
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s*\n\s*").unwrap());
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("[<>&]").unwrap());
let s = RE.replace_all(input, " ");
REGEX
.replace_all(&s, |caps: ®ex::Captures| {
match caps.get(0).unwrap().as_str() {
"<" => "<".to_owned(),
">" => ">".to_owned(),
"&" => "&".to_owned(),
_ => unreachable!(),
}
})
.into()
}
fn hex(val: u64) -> String {
format!("0x{val:x}")
}
fn progress(documented: i64, total: i64) -> String {
let f = if total > 0 {
100. * (documented as f64 / total as f64)
} else {
100.
};
if f.round() == f {
format!("{f:.1}")
} else {
f.to_string()
}
}
fn generate_index_page(devices: &Vec<Object>, writer: &mut dyn Write) -> anyhow::Result<()> {
println!("Generating Index");
let template_file = include_str!("index.template.html");
let template = liquid::ParserBuilder::with_stdlib()
.build()
.unwrap()
.parse(template_file)
.unwrap();
let globals = liquid::object!({ "devices": devices });
template.render_to(writer, &globals)?;
Ok(())
}
fn generate_device_page(
template: &liquid::Template,
device: &Object,
writer: &mut dyn Write,
) -> anyhow::Result<()> {
let globals = liquid::object!({ "device": device });
template.render_to(writer, &globals)?;
Ok(())
}
fn short_access(accs: &str) -> &str {
match accs {
"read-write" => "rw",
"read-only" => "r",
"write-only" => "w",
_ => "N/A",
}
}
trait GetI64 {
fn get_i64(&self, key: &str) -> Option<i64>;
fn get_str(&self, key: &str) -> Option<Cow<str>>;
}
impl GetI64 for Object {
fn get_i64(&self, key: &str) -> Option<i64> {
self.get(key)
.and_then(|v| v.as_view().as_scalar())
.and_then(|s| s.to_integer())
}
fn get_str(&self, key: &str) -> Option<Cow<str>> {
self.get(key)
.and_then(|v| v.as_view().as_scalar())
.map(|s| s.into_cow_str())
}
}
fn parse_cluster(
ctag: &Cluster,
registers: &mut Vec<Object>,
cpath: &BlockPath,
index: &Index,
) -> anyhow::Result<()> {
let ctag = if let Some(dfname) = ctag.derived_from.as_ref() {
let mut ctag = ctag.clone();
derive_cluster(&mut ctag, dfname, &cpath.parent().unwrap(), index)?;
Cow::Owned(ctag)
} else {
Cow::Borrowed(ctag)
};
match ctag.as_ref() {
Cluster::Single(c) => {
let mut regs: Vec<Register> = c.registers().cloned().collect();
let cluster_addr = c.address_offset;
for r in &mut regs {
let rpath = cpath.new_register(&r.name);
r.name = format!("{} [0]", r.name);
r.address_offset += cluster_addr;
parse_register_array(r, registers, &rpath, index)?;
}
}
Cluster::Array(c, d) => {
for (i, cluster_idx) in d.indexes().enumerate() {
let mut regs: Vec<Register> = c.registers().cloned().collect();
let cluster_addr = c.address_offset + (i as u32) * d.dim_increment;
for r in &mut regs {
let rpath = cpath.new_register(&r.name);
r.name = format!("{} [{cluster_idx}]", r.name);
r.address_offset += cluster_addr;
parse_register_array(r, registers, &rpath, index)?;
}
}
}
}
Ok(())
}
fn parse_register_array(
rtag: &Register,
registers: &mut Vec<Object>,
rpath: &RegisterPath,
index: &Index,
) -> anyhow::Result<()> {
let rtag = if let Some(dfname) = rtag.derived_from.as_ref() {
let mut rtag = rtag.clone();
derive_register(&mut rtag, dfname, &rpath.block, index)?;
Cow::Owned(rtag)
} else {
Cow::Borrowed(rtag)
};
match rtag.as_ref() {
Register::Single(r) => {
let register = parse_register(r, rpath, index)
.with_context(|| format!("In register {}", r.name))?;
registers.push(register);
}
Register::Array(r, d) => {
for (i, idx) in d.indexes().enumerate() {
let mut r = r.clone();
let idxs = format!("[{idx}]");
r.name = r.name.replace("[%s]", &idxs).replace("%s", &idxs);
r.address_offset += (i as u32) * d.dim_increment;
r.description = r
.description
.map(|d| d.replace("[%s]", &idx).replace("%s", &idx));
let register = parse_register(&r, rpath, index)
.with_context(|| format!("In register {}", r.name))?;
registers.push(register);
}
}
}
Ok(())
}
fn parse_register(
rtag: &RegisterInfo,
rpath: &RegisterPath,
index: &Index,
) -> anyhow::Result<Object> {
let mut register_fields_total = 0;
let mut register_fields_documented = 0;
let rsize = rtag.properties.size.unwrap_or(32);
let raccs = rtag
.properties
.access
.map(Access::as_str)
.unwrap_or("Unspecified");
let mut flds = Vec::new();
for f in rtag.fields() {
match f {
Field::Single(f) => {
flds.push(Cow::Borrowed(f));
}
Field::Array(f, d) => {
for (i, idx) in d.indexes().enumerate() {
let mut f = f.clone();
let idxs = format!("[{idx}]");
f.name = f.name.replace("[%s]", &idxs).replace("%s", &idxs);
f.bit_range = BitRange::from_offset_width(
f.bit_offset() + (i as u32) * d.dim_increment,
f.bit_width(),
);
f.description = f
.description
.map(|d| d.replace("[%s]", &idx).replace("%s", &idx));
flds.push(Cow::Owned(f));
}
}
}
}
flds.sort_by_key(|f| f.bit_offset());
let mut filling = 0_u64;
let mut fields = Vec::with_capacity(flds.len());
for ftag in &flds {
register_fields_total += 1;
let fpath = rpath.new_field(&ftag.name);
let foffset = ftag.bit_offset();
let fwidth = ftag.bit_width();
let bit_mask = (u64::MAX >> (u64::BITS - fwidth)) << foffset;
filling |= bit_mask;
let faccs = ftag.access.map(Access::as_str).unwrap_or(raccs);
let enums = ftag.enumerated_values.first();
let wc = &ftag.write_constraint;
let mut fdoc = None;
if enums.is_some() || wc.is_some() || faccs == "read-only" {
register_fields_documented += 1;
if let Some(enums) = enums {
let mut doc = "Allowed values:<br>".to_string();
let enums = if let Some(dfname) = enums.derived_from.as_ref() {
let mut enums = enums.clone();
derive_enumerated_values(&mut enums, dfname, &fpath, index)?;
Cow::Owned(enums)
} else {
Cow::Borrowed(enums)
};
for value in &enums.values {
doc += &format!(
"<strong>{}: {}</strong>: {}<br>",
value.value.unwrap(),
value.name,
sanitize(value.description.as_deref().unwrap_or(""))
);
}
fdoc = Some(doc);
} else if let Some(WriteConstraint::Range(wcrange)) = wc.as_ref() {
let mn = hex(wcrange.min);
let mx = hex(wcrange.max);
fdoc = Some(format!("Allowed values: {mn}-{mx}"));
}
}
fields.push(object!({
"name": ftag.name,
"offset": foffset,
"width": fwidth,
"msb": ftag.msb(),
"description": ftag.description.as_deref().map(sanitize),
"doc": fdoc,
"access": faccs,
}));
}
let mut table = vec![
vec![
object!({
"width": 1,
"doc": false,
"access": "",
});
16
];
2
];
for ftag in flds.iter().rev() {
let foffset = ftag.bit_offset();
let faccs = ftag.access.map(Access::as_str).unwrap_or(raccs);
let access = short_access(faccs);
let fwidth = ftag.bit_width();
if foffset + fwidth > rsize {
return Err(anyhow!("Wrong field offset/width"));
}
let fdoc = !ftag.enumerated_values.is_empty() || ftag.write_constraint.is_some();
for idx in foffset..(foffset + fwidth).min(32) {
let trowidx = ((31 - idx) / 16) as usize;
let tcolidx = (15 - (idx % 16)) as usize;
let separated = foffset < 16 && foffset + fwidth > 16;
let tcell = object!({
"name": ftag.name,
"doc": fdoc,
"access": access,
"separated": separated,
"width": table[trowidx][tcolidx].get("width"),
});
table[trowidx][tcolidx] = tcell;
}
}
for trow in table.iter_mut() {
let mut idx = 0;
while idx < trow.len() - 1 {
if trow[idx].get("name") == trow[idx + 1].get("name") {
let mut width = trow[idx].get_i64("width").unwrap();
width += 1;
trow[idx].insert("width".into(), Scalar::new(width).into());
trow.remove(idx + 1);
continue;
}
idx += 1
}
}
let table = vec![
(filling > u16::MAX as _)
.then(|| object!({"headers": (16..32).rev().collect::<Vec<_>>(), "fields": table[0]})),
(filling > 0)
.then(|| object!({"headers": (0..16).rev().collect::<Vec<_>>(), "fields": table[1]})),
];
let offset = rtag.address_offset;
Ok(object!({
"name": rtag.name,
"size": rsize,
"offset_int": offset,
"offset": hex(offset as _),
"description": rtag.description.as_deref().map(sanitize),
"resetValue": format!("0x{:08X}", rtag.properties.reset_value.unwrap_or_default()),
"access": raccs,
"writeConstraint": rtag.write_constraint,
"fields": fields,
"table": table,
"fields_total": register_fields_total,
"fields_documented": register_fields_documented,
"progress": progress(register_fields_documented, register_fields_total),
}))
}
fn parse_device(svdfile: impl AsRef<Path>) -> anyhow::Result<Object> {
let svdfile = svdfile.as_ref();
let mut file = File::open(svdfile)?;
#[cfg(not(target_os = "windows"))]
let temp = file.metadata()?.st_mtime();
#[cfg(target_os = "windows")]
let temp = file.metadata()?.last_write_time() as i64;
let mut xml = String::new();
file.read_to_string(&mut xml)?;
let device = svd_parser::parse_with_config(
&xml,
&svd_parser::Config::default().expand_properties(true),
)?;
let index = Index::create(&device);
let mut peripherals = Vec::new();
let mut device_fields_total = 0;
let mut device_fields_documented = 0;
let mut ptags = device.peripherals.iter().collect::<Vec<_>>();
ptags.sort_by_key(|p| p.name.to_lowercase());
for ptag in ptags {
let mut registers = Vec::new();
let mut peripheral_fields_total = 0;
let mut peripheral_fields_documented = 0;
let pname = &ptag.name;
let mut ppath = BlockPath::new(&ptag.name);
let ptag = if let Some(dfname) = ptag.derived_from.as_ref() {
let mut ptag = ptag.clone();
if let Some(path) = derive_peripheral(&mut ptag, dfname, &index)? {
ppath = path;
}
Cow::Owned(ptag)
} else {
Cow::Borrowed(ptag)
};
for ctag in ptag.clusters() {
let cpath = ppath.new_cluster(&ctag.name);
parse_cluster(ctag, &mut registers, &cpath, &index)
.with_context(|| format!("In cluster {}", ctag.name))
.with_context(|| format!("In peripheral {}", ptag.name))?;
}
for rtag in ptag.registers() {
let rpath = ppath.new_register(&rtag.name);
parse_register_array(rtag, &mut registers, &rpath, &index)
.with_context(|| format!("In peripheral {}", ptag.name))?;
}
registers.sort_by_key(|r| {
(
r.get_i64("offset_int"),
r.get_str("name").map(|s| s.to_lowercase()),
)
});
for register in ®isters {
peripheral_fields_total += register.get_i64("fields_total").unwrap();
peripheral_fields_documented += register.get_i64("fields_documented").unwrap();
}
peripherals.push(object!({
"name": pname,
"base": format!("0x{:08x}", ptag.base_address),
"description": ptag.description.as_deref().map(sanitize),
"registers": registers,
"fields_total": peripheral_fields_total,
"fields_documented": peripheral_fields_documented,
"progress": progress(peripheral_fields_documented, peripheral_fields_total),
}));
device_fields_total += peripheral_fields_total;
device_fields_documented += peripheral_fields_documented;
}
Ok(object!({
"name": device.name,
"peripherals": peripherals,
"fields_total": device_fields_total,
"fields_documented": device_fields_documented,
"last-modified": temp,
"svdfile": svdfile.to_str().unwrap(),
"progress": progress(device_fields_documented, device_fields_total),
}))
}
fn process_svd(svdfile: impl AsRef<Path>) -> anyhow::Result<Object> {
let svdfile = svdfile.as_ref().to_str().unwrap();
println!("Processing {}", svdfile);
parse_device(svdfile).with_context(|| format!("In file {svdfile}"))
}
fn generate_if_newer(
template: &liquid::Template,
device: &Object,
htmldir: &Path,
) -> anyhow::Result<()> {
let pagename = format!("{}.html", device.get_str("name").unwrap());
let filename = htmldir.join(&pagename);
#[cfg(not(target_os = "windows"))]
let file_mtime = if filename.is_file() {
std::fs::metadata(&filename)?.st_mtime()
} else {
i64::MIN
};
#[cfg(target_os = "windows")]
let file_mtime = if filename.is_file() {
std::fs::metadata(&filename)?.last_write_time() as i64
} else {
i64::MIN
};
if !filename.is_file() || file_mtime < device.get_i64("last-modified").unwrap() {
println!("Generating {pagename}");
let svdfile = device.get_str("svdfile").unwrap();
let svdfile = Path::new(svdfile.as_ref());
let svdfile_name = svdfile.file_name().unwrap();
let mut file = std::fs::File::create(filename)?;
generate_device_page(template, device, &mut file)?;
std::fs::copy(svdfile, htmldir.join(svdfile_name))?;
}
Ok(())
}
pub fn svd2html(htmldir: &Path, svdfiles: &[PathBuf]) -> anyhow::Result<()> {
let svdfiles = svdfiles.iter().filter(|&f| f.is_file()).collect::<Vec<_>>();
if !htmldir.exists() {
std::fs::create_dir(htmldir)?;
}
let template_file = include_str!("template.html");
let template = liquid::ParserBuilder::with_stdlib()
.build()
.unwrap()
.parse(template_file)
.unwrap();
let mut devices = svdfiles
.par_iter()
.map(|f| {
let device = process_svd(f).unwrap();
generate_if_newer(&template, &device, htmldir).unwrap();
object!({
"name": device.get("name"),
"progress": device.get("progress"),
"fields_documented": device.get("fields_documented"),
"fields_total": device.get("fields_total"),
})
})
.collect::<Vec<_>>();
devices.sort_by_key(|d| d.get_str("name").map(|s| s.to_lowercase()));
let mut file = std::fs::File::create(htmldir.join("index.html"))?;
generate_index_page(&devices, &mut file)?;
Ok(())
}