use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader};
use std::path::Path;
use std::str::FromStr;
use log::warn;
use crate::flamegraph::color::Color;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PaletteMap(HashMap<String, Color>);
impl PaletteMap {
pub fn get(&self, func: &str) -> Option<Color> {
self.0.get(func).cloned()
}
pub fn insert<S: ToString>(&mut self, func: S, color: Color) -> Option<Color> {
self.0.insert(func.to_string(), color)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, Color)> {
self.0.iter().map(|(func, color)| (func.as_str(), *color))
}
pub fn from_reader(reader: &mut dyn io::BufRead) -> io::Result<Self> {
let mut map = HashMap::default();
let mut ignored = 0;
for line in reader.lines() {
let line = line?;
if let Ok((name, color)) = parse_line(&line) {
map.insert(name.to_string(), color);
} else {
ignored += 1;
}
}
if ignored != 0 {
warn!("Ignored {} lines with invalid format", ignored);
}
Ok(PaletteMap(map))
}
pub fn to_writer(&self, writer: &mut dyn io::Write) -> io::Result<()> {
let mut entries = self.0.iter().collect::<Vec<_>>();
entries.sort_unstable();
for (name, color) in entries {
writer.write_all(
format!("{}->rgb({},{},{})\n", name, color.r, color.g, color.b).as_bytes(),
)?
}
Ok(())
}
pub fn load_from_file_or_empty(path: &dyn AsRef<Path>) -> io::Result<Self> {
if path.as_ref().exists() {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
PaletteMap::from_reader(&mut reader)
} else {
Ok(PaletteMap::default())
}
}
pub fn save_to_file(&self, path: &dyn AsRef<Path>) -> io::Result<()> {
let mut file = OpenOptions::new().write(true).create(true).open(path)?;
self.to_writer(&mut file)
}
pub(crate) fn find_color_for<F: FnMut(&str) -> Color>(
&mut self,
name: &str,
mut compute_color: F,
) -> Color {
match self.get(name) {
Some(color) => color,
None => {
let color = compute_color(name);
self.insert(name, color);
color
}
}
}
}
fn parse_line(line: &str) -> io::Result<(&str, Color)> {
let mut words = line.split("->");
let name = match words.next() {
Some(name) => name,
None => return Err(io::Error::from(io::ErrorKind::InvalidInput)),
};
let color = match words.next() {
Some(name) => name,
None => return Err(io::Error::from(io::ErrorKind::InvalidInput)),
};
if words.next().is_some() {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
}
let rgb_color =
parse_rgb_string(color).ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?;
Ok((name, rgb_color))
}
fn parse_rgb_string(s: &str) -> Option<Color> {
let s = s.trim();
if !s.starts_with("rgb(") || !s.ends_with(')') {
return None;
}
let s = &s["rgb(".len()..s.len() - 1];
let r_end_index = s.find(',')?;
let r_str = s[..r_end_index].trim();
let r = u8::from_str(r_str).ok()?;
let s = &s[r_end_index + 1..];
let g_end_index = s.find(',')?;
let g_str = s[..g_end_index].trim();
let g = u8::from_str(g_str).ok()?;
let b_str = &s[g_end_index + 1..].trim();
let b = u8::from_str(b_str).ok()?;
Some(Color { r, g, b })
}
#[cfg(test)]
mod tests {
use crate::flamegraph::color::palette_map::{parse_line, PaletteMap};
use crate::flamegraph::color::Color;
use pretty_assertions::assert_eq;
use std::io::Cursor;
macro_rules! color {
($r:expr, $g:expr, $b:expr) => {
Color {
r: $r,
g: $g,
b: $b,
}
};
}
#[test]
fn palette_map_test() {
let mut palette = PaletteMap::default();
assert_eq!(palette.insert("foo", color!(0, 50, 255)), None);
assert_eq!(palette.insert("bar", color!(50, 0, 60)), None);
assert_eq!(
palette.insert("foo", color!(80, 20, 63)),
Some(color!(0, 50, 255))
);
assert_eq!(
palette.insert("foo", color!(128, 128, 128)),
Some(color!(80, 20, 63))
);
assert_eq!(palette.insert("baz", color!(255, 0, 255)), None);
assert_eq!(palette.get("func"), None);
assert_eq!(palette.get("bar"), Some(color!(50, 0, 60)));
assert_eq!(palette.get("foo"), Some(color!(128, 128, 128)));
assert_eq!(palette.get("baz"), Some(color!(255, 0, 255)));
let mut vec = palette.iter().collect::<Vec<_>>();
vec.sort_unstable();
let mut iter = vec.iter();
assert_eq!(iter.next(), Some(&("bar", color!(50, 0, 60))));
assert_eq!(iter.next(), Some(&("baz", color!(255, 0, 255))));
assert_eq!(iter.next(), Some(&("foo", color!(128, 128, 128))));
assert_eq!(iter.next(), None);
let mut buf = Cursor::new(Vec::new());
palette.to_writer(&mut buf).unwrap();
buf.set_position(0);
let palette = PaletteMap::from_reader(&mut buf).unwrap();
let mut vec = palette.iter().collect::<Vec<_>>();
vec.sort_unstable();
let mut iter = vec.iter();
assert_eq!(iter.next(), Some(&("bar", color!(50, 0, 60))));
assert_eq!(iter.next(), Some(&("baz", color!(255, 0, 255))));
assert_eq!(iter.next(), Some(&("foo", color!(128, 128, 128))));
assert_eq!(iter.next(), None);
}
#[test]
fn parse_line_test() {
assert_eq!(
parse_line("func->rgb(0, 0, 0)").unwrap(),
("func", color!(0, 0, 0))
);
assert_eq!(
parse_line("->rgb(255, 255, 255)").unwrap(),
("", color!(255, 255, 255))
);
assert!(parse_line("").is_err());
assert!(parse_line("func->(0, 0, 0)").is_err());
assert!(parse_line("func->").is_err());
assert!(parse_line("func->foo->rgb(0, 0, 0)").is_err());
assert!(parse_line("func->rgb(0, 0, 0)->foo").is_err());
assert!(parse_line("func->rgb(255, 255, 256)").is_err());
assert!(parse_line("func->rgb(-1, 255, 255)").is_err());
}
#[test]
fn load_from_non_existing_file() {
let palette_map = PaletteMap::load_from_file_or_empty(&"non-existing-palette.map").unwrap();
assert_eq!(palette_map, PaletteMap::default());
}
}