graphviz_pure_core/
charset.rs1use std::borrow::Cow;
18use std::cell::{Cell, RefCell};
19use std::io::Write;
20
21const RAW_BASE: u32 = 0x10_FF00;
23
24pub fn decode(bytes: Vec<u8>) -> String {
26 match String::from_utf8(bytes) {
27 Ok(s) => s,
28 Err(e) => decode_slice(e.as_bytes()),
29 }
30}
31
32pub fn decode_slice(bytes: &[u8]) -> String {
34 if let Ok(s) = std::str::from_utf8(bytes) {
35 return s.to_string();
36 }
37 let mut out = String::with_capacity(bytes.len() + 8);
38 for chunk in bytes.utf8_chunks() {
39 out.push_str(chunk.valid());
40 for &b in chunk.invalid() {
41 out.push(raw_char(b));
42 }
43 }
44 out
45}
46
47pub fn raw_char(b: u8) -> char {
49 char::from_u32(RAW_BASE + u32::from(b)).expect("raw byte code point")
50}
51
52pub fn has_raw(s: &str) -> bool {
54 s.as_bytes().contains(&0xF4) && s.chars().any(|c| u32::from(c) >= RAW_BASE)
56}
57
58pub fn encode(s: &str) -> Cow<'_, [u8]> {
60 if !has_raw(s) {
61 return Cow::Borrowed(s.as_bytes());
62 }
63 let mut out = Vec::with_capacity(s.len());
64 let mut buf = [0u8; 4];
65 for c in s.chars() {
66 let v = u32::from(c);
67 if v >= RAW_BASE {
68 out.push((v - RAW_BASE) as u8);
69 } else {
70 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
71 }
72 }
73 Cow::Owned(out)
74}
75
76pub fn eprint_bytes(msg: &str) {
80 let bytes = encode(msg);
81 let bytes = bytes.split(|&b| b == 0).next().unwrap_or_default();
82 let mut out = Vec::with_capacity(bytes.len());
83 for &b in bytes {
84 if (b <= 31 || b == 127) && !matches!(b, b'\t' | b'\n' | 0x0b | 0x0c | b'\r') {
86 out.extend_from_slice(format!("\\{b:03o}").as_bytes());
87 } else {
88 out.push(b);
89 }
90 }
91 let _ = std::io::stderr().write_all(&out);
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
97pub enum Charset {
98 #[default]
100 Utf8,
101 Latin1,
103 Big5,
107}
108
109impl Charset {
110 pub fn parse(charset: Option<&str>) -> (Self, bool) {
113 let p = charset.unwrap_or("utf-8");
114 let is = |names: &[&str]| names.iter().any(|n| n.eq_ignore_ascii_case(p));
115 if is(&[
116 "latin-1",
117 "latin1",
118 "l1",
119 "ISO-8859-1",
120 "ISO_8859-1",
121 "ISO8859-1",
122 "ISO-IR-100",
123 ]) {
124 (Self::Latin1, true)
125 } else if is(&["big-5", "big5"]) {
126 (Self::Big5, true)
127 } else if is(&["utf-8", "utf8"]) {
128 (Self::Utf8, true)
129 } else {
130 (Self::Utf8, false)
131 }
132 }
133
134 pub fn from_attr(charset: Option<&str>) -> Self {
136 Self::parse(charset).0
137 }
138}
139
140thread_local! {
141 static CURRENT: Cell<Charset> = const { Cell::new(Charset::Utf8) };
142 static GRAPH: RefCell<String> = const { RefCell::new(String::new()) };
143 static WARNED: Cell<bool> = const { Cell::new(false) };
144}
145
146pub fn begin_layout(graph_name: &str, charset: Option<&str>) {
150 let (cs, ok) = Charset::parse(charset);
151 if !ok {
152 eprint_bytes(&format!(
153 "Warning: Unsupported charset \"{}\" - assuming utf-8\n",
154 charset.unwrap_or("")
155 ));
156 }
157 CURRENT.with(|c| c.set(cs));
158 WARNED.with(|w| w.set(false));
159 GRAPH.with(|g| {
160 let mut g = g.borrow_mut();
161 g.clear();
162 g.push_str(graph_name);
163 });
164}
165
166pub fn begin_render(graph_name: &str, charset: Option<&str>) {
168 let cs = Charset::from_attr(charset);
169 CURRENT.with(|c| c.set(cs));
170 GRAPH.with(|g| {
171 let mut g = g.borrow_mut();
172 if *g != graph_name {
173 g.clear();
174 g.push_str(graph_name);
175 WARNED.with(|w| w.set(false));
176 }
177 });
178}
179
180pub fn current() -> Charset {
182 CURRENT.with(Cell::get)
183}
184
185pub(crate) fn warn_invalid_utf8(msg: impl FnOnce(&str) -> String) {
187 if WARNED.with(|w| w.replace(true)) {
188 return;
189 }
190 let text = GRAPH.with(|g| msg(&g.borrow()));
191 eprint_bytes(&text);
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn round_trip() {
200 let bytes = b"caf\xe9 \xc3\xa9 \xe4\xbcz \xff".to_vec();
201 let s = decode(bytes.clone());
202 assert!(s.contains('\u{e9}'));
203 assert!(has_raw(&s));
204 assert_eq!(&*encode(&s), &bytes[..]);
205 assert_eq!(decode(b"plain".to_vec()), "plain");
206 assert!(matches!(encode("plain \u{10ffff}x"), Cow::Owned(_)));
207 }
208
209 #[test]
210 fn find_charset() {
211 assert_eq!(Charset::parse(Some("ISO-8859-1")), (Charset::Latin1, true));
212 assert_eq!(Charset::parse(Some("BIG5")), (Charset::Big5, true));
213 assert_eq!(Charset::parse(None), (Charset::Utf8, true));
214 assert_eq!(Charset::parse(Some("koi8-r")), (Charset::Utf8, false));
215 }
216}