term_transcript/
utils.rs

1//! Misc utils.
2
3use std::{borrow::Cow, fmt::Write as WriteStr, io, str};
4
5#[cfg(any(feature = "svg", feature = "test"))]
6pub use self::rgb_color::RgbColor;
7#[cfg(feature = "svg")]
8pub use self::rgb_color::RgbColorParseError;
9
10/// Adapter for `dyn fmt::Write` that implements `io::Write`.
11pub(crate) struct WriteAdapter<'a> {
12    inner: &'a mut dyn WriteStr,
13}
14
15impl<'a> WriteAdapter<'a> {
16    pub fn new(output: &'a mut dyn WriteStr) -> Self {
17        Self { inner: output }
18    }
19}
20
21impl io::Write for WriteAdapter<'_> {
22    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
23        let segment =
24            str::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
25        self.inner.write_str(segment).map_err(io::Error::other)?;
26        Ok(buf.len())
27    }
28
29    fn flush(&mut self) -> io::Result<()> {
30        Ok(())
31    }
32}
33
34pub(crate) fn normalize_newlines(s: &str) -> Cow<'_, str> {
35    if s.contains("\r\n") {
36        Cow::Owned(s.replace("\r\n", "\n"))
37    } else {
38        Cow::Borrowed(s)
39    }
40}
41
42#[cfg(not(windows))]
43pub(crate) fn is_recoverable_kill_error(err: &io::Error) -> bool {
44    matches!(err.kind(), io::ErrorKind::InvalidInput)
45}
46
47// As per `TerminateProcess` docs (`TerminateProcess` is used by `Child::kill()`),
48// the call will result in ERROR_ACCESS_DENIED if the process has already terminated.
49//
50// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess
51#[cfg(windows)]
52pub(crate) fn is_recoverable_kill_error(err: &io::Error) -> bool {
53    matches!(
54        err.kind(),
55        io::ErrorKind::InvalidInput | io::ErrorKind::PermissionDenied
56    )
57}
58
59#[cfg(any(feature = "svg", feature = "test"))]
60mod rgb_color {
61    use std::{error::Error as StdError, fmt, num::ParseIntError, str::FromStr};
62
63    /// RGB color with 8-bit channels.
64    ///
65    /// A color [can be parsed](FromStr) from a hex string like `#fed` or `#de382b`.
66    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67    pub struct RgbColor(pub u8, pub u8, pub u8);
68
69    impl fmt::LowerHex for RgbColor {
70        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71            write!(formatter, "#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
72        }
73    }
74
75    /// Errors that can occur when [parsing](FromStr) an [`RgbColor`] from a string.
76    #[derive(Debug)]
77    #[non_exhaustive]
78    pub enum RgbColorParseError {
79        /// Color string contains non-ASCII chars.
80        NotAscii,
81        /// The color does not have a `#` prefix.
82        NoHashPrefix,
83        /// The color has incorrect string length (not 1 or 2 chars per color channel).
84        /// The byte length of the string (including 1 char for the `#` prefix)
85        /// is provided within this variant.
86        IncorrectLen(usize),
87        /// Error parsing color channel value.
88        IncorrectDigit(ParseIntError),
89    }
90
91    impl fmt::Display for RgbColorParseError {
92        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93            match self {
94                Self::NotAscii => formatter.write_str("color string contains non-ASCII chars"),
95                Self::NoHashPrefix => formatter.write_str("missing '#' prefix"),
96                Self::IncorrectLen(len) => write!(
97                    formatter,
98                    "unexpected byte length {len} of color string, expected 4 or 7"
99                ),
100                Self::IncorrectDigit(err) => write!(formatter, "error parsing hex digit: {err}"),
101            }
102        }
103    }
104
105    impl StdError for RgbColorParseError {
106        fn source(&self) -> Option<&(dyn StdError + 'static)> {
107            match self {
108                Self::IncorrectDigit(err) => Some(err),
109                _ => None,
110            }
111        }
112    }
113
114    impl FromStr for RgbColor {
115        type Err = RgbColorParseError;
116
117        fn from_str(s: &str) -> Result<Self, Self::Err> {
118            if s.is_empty() || s.as_bytes()[0] != b'#' {
119                Err(RgbColorParseError::NoHashPrefix)
120            } else if s.len() == 4 {
121                if !s.is_ascii() {
122                    return Err(RgbColorParseError::NotAscii);
123                }
124
125                let r =
126                    u8::from_str_radix(&s[1..2], 16).map_err(RgbColorParseError::IncorrectDigit)?;
127                let g =
128                    u8::from_str_radix(&s[2..3], 16).map_err(RgbColorParseError::IncorrectDigit)?;
129                let b =
130                    u8::from_str_radix(&s[3..], 16).map_err(RgbColorParseError::IncorrectDigit)?;
131                Ok(Self(r * 17, g * 17, b * 17))
132            } else if s.len() == 7 {
133                if !s.is_ascii() {
134                    return Err(RgbColorParseError::NotAscii);
135                }
136
137                let r =
138                    u8::from_str_radix(&s[1..3], 16).map_err(RgbColorParseError::IncorrectDigit)?;
139                let g =
140                    u8::from_str_radix(&s[3..5], 16).map_err(RgbColorParseError::IncorrectDigit)?;
141                let b =
142                    u8::from_str_radix(&s[5..], 16).map_err(RgbColorParseError::IncorrectDigit)?;
143                Ok(Self(r, g, b))
144            } else {
145                Err(RgbColorParseError::IncorrectLen(s.len()))
146            }
147        }
148    }
149}
150
151#[cfg(all(test, any(feature = "svg", feature = "test")))]
152mod tests {
153    use assert_matches::assert_matches;
154
155    use super::*;
156
157    #[test]
158    fn parsing_color() {
159        let RgbColor(r, g, b) = "#fed".parse().unwrap();
160        assert_eq!((r, g, b), (0xff, 0xee, 0xdd));
161        let RgbColor(r, g, b) = "#c0ffee".parse().unwrap();
162        assert_eq!((r, g, b), (0xc0, 0xff, 0xee));
163    }
164
165    #[test]
166    fn errors_parsing_color() {
167        let err = "123".parse::<RgbColor>().unwrap_err();
168        assert_matches!(err, RgbColorParseError::NoHashPrefix);
169        let err = "#12".parse::<RgbColor>().unwrap_err();
170        assert_matches!(err, RgbColorParseError::IncorrectLen(3));
171        let err = "#тэг".parse::<RgbColor>().unwrap_err();
172        assert_matches!(err, RgbColorParseError::NotAscii);
173        let err = "#coffee".parse::<RgbColor>().unwrap_err();
174        assert_matches!(err, RgbColorParseError::IncorrectDigit(_));
175    }
176}