1use crate::error::ResolveError;
15use crate::resolver::ResolveOutcome;
16use base64::Engine;
17
18const FONT: &str = "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif";
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ErrorImage {
24 Integrity,
26 Unreachable,
28 NotFound,
30 InvalidUrn,
32 Generic,
34}
35
36pub fn for_outcome(outcome: &ResolveOutcome) -> ErrorImage {
39 match outcome {
40 ResolveOutcome::Success(_) => ErrorImage::Generic, ResolveOutcome::IntegrityFailure => ErrorImage::Integrity,
42 ResolveOutcome::Unreachable => ErrorImage::Unreachable,
43 }
44}
45
46pub fn for_error(err: &ResolveError) -> ErrorImage {
48 match err {
49 ResolveError::Parse(_) | ResolveError::RootRequired => ErrorImage::InvalidUrn,
50 ResolveError::NotFound => ErrorImage::NotFound,
51 ResolveError::Transport(_) => ErrorImage::Unreachable,
54 ResolveError::VerifyFailed(_) | ResolveError::DecryptFailed => ErrorImage::Integrity,
55 ResolveError::Rpc(_) => ErrorImage::Generic,
56 }
57}
58
59fn treatment(
62 kind: ErrorImage,
63) -> (
64 &'static str,
65 &'static str,
66 &'static str,
67 &'static str,
68 &'static str,
69) {
70 match kind {
71 ErrorImage::Integrity => (
73 "#e5484d",
74 "#3a1416",
75 "!",
76 "Verification failed",
77 "Content could not be trusted",
78 ),
79 ErrorImage::Unreachable => (
80 "#3b6cf6",
81 "#14213a",
82 "\u{21bb}",
83 "Network unreachable",
84 "Connect a DIG node",
85 ),
86 ErrorImage::NotFound => ("#7f8db3", "#141a2e", "?", "Content not found", ""),
87 ErrorImage::InvalidUrn => ("#7f8db3", "#141a2e", "!", "Invalid DIG URN", ""),
88 ErrorImage::Generic => ("#7f8db3", "#141a2e", "!", "Something went wrong", ""),
89 }
90}
91
92pub fn png(kind: ErrorImage) -> &'static [u8] {
100 match kind {
101 ErrorImage::Integrity => include_bytes!("../assets/error-integrity.png"),
102 ErrorImage::Unreachable => include_bytes!("../assets/error-unreachable.png"),
103 ErrorImage::NotFound => include_bytes!("../assets/error-not-found.png"),
104 ErrorImage::InvalidUrn => include_bytes!("../assets/error-invalid-urn.png"),
105 ErrorImage::Generic => include_bytes!("../assets/error-generic.png"),
106 }
107}
108
109pub fn data_uri(kind: ErrorImage) -> String {
111 let b64 = base64::engine::general_purpose::STANDARD.encode(png(kind));
112 format!("data:image/png;base64,{b64}")
113}
114
115pub fn svg(kind: ErrorImage) -> String {
118 let (accent, glow, glyph, title, subtitle) = treatment(kind);
119 let subtitle_el = if subtitle.is_empty() {
120 String::new()
121 } else {
122 format!(
123 "<text x='256' y='372' text-anchor='middle' font-family='{FONT}' font-size='26' fill='#aeb7d4'>{subtitle}</text>"
124 )
125 };
126 format!(
127 r##"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' width='512' height='512' role='img' aria-label='DIG Network: {title}'>
128 <defs><radialGradient id='bg' cx='50%' cy='0%' r='120%'>
129 <stop offset='0%' stop-color='{glow}'/><stop offset='60%' stop-color='#0a0f1e'/><stop offset='100%' stop-color='#070a14'/>
130 </radialGradient></defs>
131 <rect width='512' height='512' fill='url(#bg)'/>
132 <circle cx='256' cy='190' r='68' fill='none' stroke='{accent}' stroke-width='8'/>
133 <text x='256' y='226' text-anchor='middle' font-family='{FONT}' font-size='84' font-weight='700' fill='{accent}'>{glyph}</text>
134 <text x='256' y='320' text-anchor='middle' font-family='{FONT}' font-size='40' font-weight='700' fill='#ffffff'>{title}</text>
135 {subtitle_el}
136 <text x='256' y='452' text-anchor='middle' font-family='{FONT}' font-size='22' font-weight='700' letter-spacing='6' fill='{accent}'>DIG NETWORK</text>
137 <text x='256' y='478' text-anchor='middle' font-family='{FONT}' font-size='16' letter-spacing='2' fill='#7f8db3'>dig.net</text>
138</svg>"##
139 )
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use base64::Engine;
146
147 const PNG_MAGIC: &[u8] = &[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
148
149 #[test]
150 fn every_kind_embeds_a_png_data_uri() {
151 for kind in [
152 ErrorImage::Integrity,
153 ErrorImage::Unreachable,
154 ErrorImage::NotFound,
155 ErrorImage::InvalidUrn,
156 ErrorImage::Generic,
157 ] {
158 assert!(png(kind).starts_with(PNG_MAGIC), "PNG magic present");
160 let uri = data_uri(kind);
161 assert!(uri.starts_with("data:image/png;base64,"));
162 let b64 = uri.trim_start_matches("data:image/png;base64,");
164 let decoded = base64::engine::general_purpose::STANDARD
165 .decode(b64)
166 .unwrap();
167 assert!(decoded.starts_with(PNG_MAGIC));
168 }
169 }
170
171 #[test]
172 fn svg_design_source_is_labelled_and_square() {
173 for kind in [
174 ErrorImage::Integrity,
175 ErrorImage::Unreachable,
176 ErrorImage::NotFound,
177 ErrorImage::InvalidUrn,
178 ErrorImage::Generic,
179 ] {
180 let svg = svg(kind);
181 assert!(svg.starts_with("<svg"));
182 assert!(svg.contains("DIG NETWORK"), "wordmark present");
183 assert!(svg.contains("viewBox='0 0 512 512'"), "square + scalable");
184 }
185 }
186
187 #[test]
188 fn labels_match_outcome() {
189 assert!(svg(ErrorImage::Integrity).contains("Verification failed"));
190 assert!(svg(ErrorImage::Unreachable).contains("Network unreachable"));
191 assert!(svg(ErrorImage::Unreachable).contains("Connect a DIG node"));
192 assert!(svg(ErrorImage::NotFound).contains("Content not found"));
193 assert!(svg(ErrorImage::InvalidUrn).contains("Invalid DIG URN"));
194 assert!(svg(ErrorImage::Generic).contains("Something went wrong"));
195 }
196
197 #[test]
198 fn outcome_and_error_mapping() {
199 assert_eq!(
200 for_outcome(&ResolveOutcome::IntegrityFailure),
201 ErrorImage::Integrity
202 );
203 assert_eq!(
204 for_outcome(&ResolveOutcome::Unreachable),
205 ErrorImage::Unreachable
206 );
207 assert_eq!(for_error(&ResolveError::NotFound), ErrorImage::NotFound);
208 assert_eq!(
209 for_error(&ResolveError::Parse("x".into())),
210 ErrorImage::InvalidUrn
211 );
212 assert_eq!(
213 for_error(&ResolveError::RootRequired),
214 ErrorImage::InvalidUrn
215 );
216 assert_eq!(
217 for_error(&ResolveError::Rpc("x".into())),
218 ErrorImage::Generic
219 );
220 assert_eq!(
221 for_error(&ResolveError::DecryptFailed),
222 ErrorImage::Integrity
223 );
224 }
225
226 #[test]
227 fn integrity_image_is_static_never_carries_bytes() {
228 let uri = data_uri(ErrorImage::Integrity);
231 assert!(!uri.contains("TAMPERED_SECRET_PAYLOAD"));
232 assert_eq!(uri, data_uri(ErrorImage::Integrity));
234 assert_eq!(png(ErrorImage::Integrity), png(ErrorImage::Integrity));
235 }
236}