1use std::{borrow::Cow, fmt::Write as WriteStr, io, str};
4
5#[cfg(any(feature = "svg", feature = "test"))]
6pub(crate) use self::rgb_color::IndexOrRgb;
7#[cfg(any(feature = "svg", feature = "test"))]
8pub use self::rgb_color::RgbColor;
9#[cfg(feature = "svg")]
10pub use self::rgb_color::RgbColorParseError;
11
12pub(crate) struct WriteAdapter<'a> {
14 inner: &'a mut dyn WriteStr,
15}
16
17impl<'a> WriteAdapter<'a> {
18 pub fn new(output: &'a mut dyn WriteStr) -> Self {
19 Self { inner: output }
20 }
21}
22
23impl io::Write for WriteAdapter<'_> {
24 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
25 let segment =
26 str::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
27 self.inner.write_str(segment).map_err(io::Error::other)?;
28 Ok(buf.len())
29 }
30
31 fn flush(&mut self) -> io::Result<()> {
32 Ok(())
33 }
34}
35
36pub(crate) fn normalize_newlines(s: &str) -> Cow<'_, str> {
37 if s.contains("\r\n") {
38 Cow::Owned(s.replace("\r\n", "\n"))
39 } else {
40 Cow::Borrowed(s)
41 }
42}
43
44#[cfg(not(windows))]
45pub(crate) fn is_recoverable_kill_error(err: &io::Error) -> bool {
46 matches!(err.kind(), io::ErrorKind::InvalidInput)
47}
48
49#[cfg(windows)]
54pub(crate) fn is_recoverable_kill_error(err: &io::Error) -> bool {
55 matches!(
56 err.kind(),
57 io::ErrorKind::InvalidInput | io::ErrorKind::PermissionDenied
58 )
59}
60
61#[cfg(any(feature = "svg", feature = "test"))]
62mod rgb_color {
63 use std::{error::Error as StdError, fmt, num::ParseIntError, str::FromStr};
64
65 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69 pub struct RgbColor(pub u8, pub u8, pub u8);
70
71 impl fmt::LowerHex for RgbColor {
72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(formatter, "#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
74 }
75 }
76
77 #[derive(Debug)]
79 #[non_exhaustive]
80 pub enum RgbColorParseError {
81 NotAscii,
83 NoHashPrefix,
85 IncorrectLen(usize),
89 IncorrectDigit(ParseIntError),
91 }
92
93 impl fmt::Display for RgbColorParseError {
94 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 Self::NotAscii => formatter.write_str("color string contains non-ASCII chars"),
97 Self::NoHashPrefix => formatter.write_str("missing '#' prefix"),
98 Self::IncorrectLen(len) => write!(
99 formatter,
100 "unexpected byte length {len} of color string, expected 4 or 7"
101 ),
102 Self::IncorrectDigit(err) => write!(formatter, "error parsing hex digit: {err}"),
103 }
104 }
105 }
106
107 impl StdError for RgbColorParseError {
108 fn source(&self) -> Option<&(dyn StdError + 'static)> {
109 match self {
110 Self::IncorrectDigit(err) => Some(err),
111 _ => None,
112 }
113 }
114 }
115
116 impl FromStr for RgbColor {
117 type Err = RgbColorParseError;
118
119 fn from_str(s: &str) -> Result<Self, Self::Err> {
120 if s.is_empty() || s.as_bytes()[0] != b'#' {
121 Err(RgbColorParseError::NoHashPrefix)
122 } else if s.len() == 4 {
123 if !s.is_ascii() {
124 return Err(RgbColorParseError::NotAscii);
125 }
126
127 let r =
128 u8::from_str_radix(&s[1..2], 16).map_err(RgbColorParseError::IncorrectDigit)?;
129 let g =
130 u8::from_str_radix(&s[2..3], 16).map_err(RgbColorParseError::IncorrectDigit)?;
131 let b =
132 u8::from_str_radix(&s[3..], 16).map_err(RgbColorParseError::IncorrectDigit)?;
133 Ok(Self(r * 17, g * 17, b * 17))
134 } else if s.len() == 7 {
135 if !s.is_ascii() {
136 return Err(RgbColorParseError::NotAscii);
137 }
138
139 let r =
140 u8::from_str_radix(&s[1..3], 16).map_err(RgbColorParseError::IncorrectDigit)?;
141 let g =
142 u8::from_str_radix(&s[3..5], 16).map_err(RgbColorParseError::IncorrectDigit)?;
143 let b =
144 u8::from_str_radix(&s[5..], 16).map_err(RgbColorParseError::IncorrectDigit)?;
145 Ok(Self(r, g, b))
146 } else {
147 Err(RgbColorParseError::IncorrectLen(s.len()))
148 }
149 }
150 }
151
152 #[derive(Debug, Clone, Copy, PartialEq)]
153 #[cfg_attr(feature = "svg", derive(serde::Serialize))]
154 #[cfg_attr(feature = "svg", serde(untagged))]
155 pub(crate) enum IndexOrRgb {
156 Index(u8),
157 Rgb(RgbColor),
158 }
159
160 impl IndexOrRgb {
161 #[cfg(feature = "svg")]
162 #[allow(clippy::match_wildcard_for_single_variants)]
163 pub(crate) fn new(color: termcolor::Color) -> std::io::Result<Self> {
165 use termcolor::Color;
166
167 Ok(match color {
168 Color::Black => Self::index(0),
169 Color::Red => Self::index(1),
170 Color::Green => Self::index(2),
171 Color::Yellow => Self::index(3),
172 Color::Blue => Self::index(4),
173 Color::Magenta => Self::index(5),
174 Color::Cyan => Self::index(6),
175 Color::White => Self::index(7),
176 Color::Ansi256(idx) => Self::indexed_color(idx),
177 Color::Rgb(r, g, b) => Self::Rgb(RgbColor(r, g, b)),
178 _ => return Err(std::io::Error::other("Unsupported color")),
179 })
180 }
181
182 fn index(value: u8) -> Self {
183 debug_assert!(value < 16);
184 Self::Index(value)
185 }
186
187 pub(crate) fn indexed_color(index: u8) -> Self {
188 match index {
189 0..=15 => Self::index(index),
190
191 16..=231 => {
192 let index = index - 16;
193 let r = Self::color_cube_color(index / 36);
194 let g = Self::color_cube_color((index / 6) % 6);
195 let b = Self::color_cube_color(index % 6);
196 Self::Rgb(RgbColor(r, g, b))
197 }
198
199 _ => {
200 let gray = 10 * (index - 232) + 8;
201 Self::Rgb(RgbColor(gray, gray, gray))
202 }
203 }
204 }
205
206 fn color_cube_color(index: u8) -> u8 {
207 match index {
208 0 => 0,
209 1 => 0x5f,
210 2 => 0x87,
211 3 => 0xaf,
212 4 => 0xd7,
213 5 => 0xff,
214 _ => unreachable!(),
215 }
216 }
217 }
218}
219
220#[cfg(all(test, any(feature = "svg", feature = "test")))]
221mod tests {
222 use assert_matches::assert_matches;
223
224 use super::*;
225
226 #[test]
227 fn parsing_color() {
228 let RgbColor(r, g, b) = "#fed".parse().unwrap();
229 assert_eq!((r, g, b), (0xff, 0xee, 0xdd));
230 let RgbColor(r, g, b) = "#c0ffee".parse().unwrap();
231 assert_eq!((r, g, b), (0xc0, 0xff, 0xee));
232 }
233
234 #[test]
235 fn errors_parsing_color() {
236 let err = "123".parse::<RgbColor>().unwrap_err();
237 assert_matches!(err, RgbColorParseError::NoHashPrefix);
238 let err = "#12".parse::<RgbColor>().unwrap_err();
239 assert_matches!(err, RgbColorParseError::IncorrectLen(3));
240 let err = "#тэг".parse::<RgbColor>().unwrap_err();
241 assert_matches!(err, RgbColorParseError::NotAscii);
242 let err = "#coffee".parse::<RgbColor>().unwrap_err();
243 assert_matches!(err, RgbColorParseError::IncorrectDigit(_));
244 }
245}