1use std::fmt::Debug;
2use std::fmt::Display;
3use std::path::Path;
4
5use arqoii::types::QoiHeader;
6use base64::Engine;
7
8#[cfg(feature = "cli")]
9use clap::{builder::PossibleValue, ValueEnum};
10
11use image::ImageBuffer;
12use image::Luma;
13use qrcode::render::Pixel;
14use qrcode::QrCode;
15
16#[derive(Clone)]
17#[non_exhaustive]
18pub enum ImageFormat {
19 #[non_exhaustive]
20 ImageFormat(image::ImageFormat),
21 #[cfg(feature = "qoi")]
22 #[non_exhaustive]
23 Qoi,
24}
25
26impl Debug for ImageFormat {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 ImageFormat::ImageFormat(format) => write!(f, "{format:?}"),
30 ImageFormat::Qoi => write!(f, "Qoi"),
31 }
32 }
33}
34
35impl Default for ImageFormat {
36 fn default() -> Self {
37 Self::ImageFormat(image::ImageFormat::Png)
38 }
39}
40
41#[cfg(feature = "cli")]
42impl ValueEnum for ImageFormat {
43 fn value_variants<'a>() -> &'a [Self] {
44 &[
45 Self::Qoi,
46 Self::ImageFormat(image::ImageFormat::Png),
47 Self::ImageFormat(image::ImageFormat::Jpeg),
48 ]
49 }
50
51 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
52 let name = format!("{self:?}").to_lowercase();
53 Some(PossibleValue::new(name))
54 }
55}
56
57impl ImageFormat {
58 pub fn png() -> Self {
59 Self::ImageFormat(image::ImageFormat::Png)
60 }
61
62 #[cfg(feature = "qoi")]
63 pub fn qoi() -> Self {
64 Self::Qoi
65 }
66}
67
68struct Image {
69 buffer: ImageBuffer<Luma<u8>, Vec<u8>>,
70}
71
72impl Image {
73 pub fn save(&self, format: ImageFormat, file_path: &Path) -> Result<(), GenerationError> {
74 match format {
75 ImageFormat::ImageFormat(format) => {
76 self.buffer.save_with_format(file_path, format)?;
77 }
78 ImageFormat::Qoi => {
79 let data = arqoii::encode::QoiEncoder::new(
80 QoiHeader::new(
81 self.buffer.width(),
82 self.buffer.height(),
83 arqoii::types::QoiChannels::Rgb,
84 arqoii::types::QoiColorSpace::SRgbWithLinearAlpha,
85 ),
86 self.buffer.pixels().map(|px| arqoii::types::Pixel {
87 r: px.0[0],
88 g: px.0[0],
89 b: px.0[0],
90 a: 255,
91 }),
92 )
93 .collect::<Vec<_>>();
94 std::fs::write(file_path, data)?;
95 }
96 }
97 Ok(())
98 }
99 pub fn save_guess_format(&self, file_path: &Path) -> Result<(), GenerationError> {
100 if cfg!(feature = "qoi") && file_path.extension().is_some_and(|ext| ext == "qoi") {
101 self.save(ImageFormat::Qoi, file_path)
102 } else {
103 self.buffer.save(file_path)?;
104 Ok(())
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy)]
110struct Px(Luma<u8>);
111
112struct Canvas(Px, Image);
113
114impl Pixel for Px {
115 type Image = Image;
116
117 type Canvas = Canvas;
118
119 fn default_color(color: qrcode::Color) -> Self {
120 Self(Luma([color.select(0, 255)]))
121 }
122}
123
124impl qrcode::render::Canvas for Canvas {
125 type Pixel = Px;
126
127 type Image = <Px as Pixel>::Image;
128
129 fn new(width: u32, height: u32, dark_pixel: Self::Pixel, light_pixel: Self::Pixel) -> Self {
130 Self(
131 dark_pixel,
132 Image {
133 buffer: ImageBuffer::from_pixel(width, height, light_pixel.0),
134 },
135 )
136 }
137
138 fn draw_dark_pixel(&mut self, x: u32, y: u32) {
139 self.1.buffer.put_pixel(x, y, self.0 .0)
140 }
141
142 fn into_image(self) -> Self::Image {
143 self.1
144 }
145}
146
147#[derive(Debug, thiserror::Error)]
148pub enum GenerationError {
149 #[error("{0}")]
150 QrError(#[from] qrcode::types::QrError),
151 #[error("{0}")]
152 ImageError(#[from] image::error::ImageError),
153 #[error("{0}")]
154 Io(#[from] std::io::Error),
155}
156
157#[derive(Debug, Clone)]
158pub struct Wifi {
159 ssid: String,
160 kind: Option<WifiMethod>,
161 hidden: bool,
162 eap_method: Option<EapMethod>,
163 phase2: Option<Phase2>,
164 anonymous_identity: Option<String>,
165 identity: Option<String>,
166 password: Option<String>,
167 public_key: Option<Vec<u8>>,
168}
169
170impl Wifi {
171 pub fn new(ssid: String) -> Self {
172 Self {
173 ssid,
174 kind: None,
175 hidden: false,
176 eap_method: None,
177 phase2: None,
178 anonymous_identity: None,
179 identity: None,
180 password: None,
181 public_key: None,
182 }
183 }
184
185 pub fn with_method(mut self, wifi_method: Option<WifiMethod>) -> Self {
186 self.kind = wifi_method;
187 self
188 }
189
190 pub fn with_hidden(mut self, hidden: bool) -> Self {
191 self.hidden = hidden;
192 self
193 }
194
195 pub fn with_eap_method(mut self, eap: Option<EapMethod>) -> Self {
196 self.eap_method = eap;
197 self
198 }
199
200 pub fn with_phase2(mut self, ph2: Option<Phase2>) -> Self {
201 self.phase2 = ph2;
202 self
203 }
204
205 pub fn with_anonymous_identity(mut self, anon: Option<String>) -> Self {
206 self.anonymous_identity = anon;
207 self
208 }
209
210 pub fn with_identity(mut self, id: Option<String>) -> Self {
211 self.identity = id;
212 self
213 }
214
215 pub fn with_password(mut self, pw: Option<String>) -> Self {
216 self.password = pw;
217 self
218 }
219
220 pub fn with_public_key(mut self, pk: Option<Vec<u8>>) -> Self {
221 self.public_key = pk;
222 self
223 }
224
225 pub fn generate_image_file(
226 &self,
227 format: Option<ImageFormat>,
228 file_path: &Path,
229 ) -> Result<(), GenerationError> {
230 let code = QrCode::new(self.to_string())?;
231
232 let image = code.render::<Px>().build();
233
234 match format {
235 Some(format) => image.save(format, file_path)?,
236 None => image.save_guess_format(file_path)?,
237 }
238
239 Ok(())
240 }
241
242 fn expected_field_count(&self) -> usize {
243 self.kind.as_ref().map_or(0, |method|if let WifiMethod::Wpa3 = method {
244 2
245 } else {
246 1
247 })
248 + 1 + self.hidden as usize
250 + self.eap_method.is_some() as usize
251 + self.phase2.is_some() as usize
252 + self.anonymous_identity.is_some() as usize
253 + self.identity.is_some() as usize
254 + self.password.is_some() as usize
255 + self.public_key.is_some() as usize
256 }
257
258 fn fields(&self) -> Vec<Field> {
259 let expected_fields = self.expected_field_count();
260
261 let mut fields = Vec::with_capacity(expected_fields);
262
263 if let Some(kind) = &self.kind {
264 kind.add_fields(&mut fields);
265 }
266
267 fields.push(Field::new_string("S", &self.ssid));
268
269 if self.hidden {
270 fields.push(Field::new_string("H", "true"))
271 }
272
273 if let Some(eap) = &self.eap_method {
274 eap.add_fields(&mut fields);
275 }
276
277 if let Some(ph2) = &self.phase2 {
278 ph2.add_fields(&mut fields)
279 }
280
281 if let Some(anon) = &self.anonymous_identity {
282 fields.push(Field::new_string("A", anon));
283 }
284
285 if let Some(ident) = &self.identity {
286 fields.push(Field::new_string("I", ident));
287 }
288
289 if let Some(password) = &self.password {
290 fields.push(Field::new_string("P", password));
291 }
292
293 if let Some(pk) = &self.public_key {
294 fields.push(Field::new_base64("K", pk));
295 }
296
297 fields
298 }
299}
300
301impl ToString for Wifi {
302 fn to_string(&self) -> String {
303 let content: String = self.fields().into_iter().map(|f| f.to_string()).collect();
304 format!("WIFI:{content};")
305 }
306}
307
308pub struct Field {
309 name: String,
310 value: String,
311}
312
313impl Field {
314 fn new_string(name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
315 Self {
316 name: name.as_ref().to_string(),
317 value: Self::escape_field_value(value.as_ref()),
318 }
319 }
320
321 fn new_base64(name: impl AsRef<str>, value: impl AsRef<[u8]>) -> Self {
322 Self {
323 name: name.as_ref().to_string(),
324 value: base64::engine::general_purpose::STANDARD.encode(value),
325 }
326 }
327
328 fn new_hex(name: impl AsRef<str>, value: impl AsRef<[u8]>) -> Self {
329 Self {
330 name: name.as_ref().to_string(),
331 value: value.as_ref().iter().map(|b| format!("{b:x}")).collect(),
332 }
333 }
334
335 fn escape_field_value(value: &str) -> String {
336 let value = value
338 .replace('\\', "\\\\")
339 .replace(';', "\\;")
340 .replace(',', "\\,")
341 .replace('"', "\\\"")
342 .replace(':', "\\:");
343
344 if Self::could_be_ascii_hex(&value) {
345 format!("\"{value}\"")
346 } else {
347 value
348 }
349 }
350
351 fn could_be_ascii_hex(value: &str) -> bool {
352 for c in value.chars() {
353 if !"0123456789abcdef".contains(c) {
354 return false;
355 }
356 }
357 true
358 }
359}
360
361impl Display for Field {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 write!(f, "{}:{};", self.name, self.value)
364 }
365}
366
367#[derive(Debug, Clone)]
368#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
369#[non_exhaustive]
370pub enum WifiMethod {
371 NoPass,
372 Wep,
373 Wpa,
375 Wpa2Enterprise,
376 Wpa3,
378}
379
380impl WifiMethod {
381 pub fn add_fields(&self, fields: &mut Vec<Field>) {
382 let kind = match self {
383 WifiMethod::NoPass => "nopass",
384 WifiMethod::Wep => "WEP",
385 WifiMethod::Wpa
386 | WifiMethod::Wpa3 => "WPA",
388 WifiMethod::Wpa2Enterprise => "WPA2-EAP",
389 };
390
391 fields.push(Field::new_string("T", kind));
392
393 if let WifiMethod::Wpa3 = self {
394 fields.push(Field::new_hex("R", [1]))
398 }
399 }
400}
401
402#[derive(Debug, Clone)]
403#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
404#[non_exhaustive]
405pub enum EapMethod {
406 Peap,
407 Tls,
408 Ttls,
409 Pwd,
410 Sim,
411 Aka,
412 AkaPrime,
413}
414
415impl EapMethod {
416 pub fn add_fields(&self, fields: &mut Vec<Field>) {
417 let eap_name = match self {
418 EapMethod::Peap => "PEAP",
419 EapMethod::Tls => "TLS",
420 EapMethod::Ttls => "TTLS",
421 EapMethod::Pwd => "PWD",
422 EapMethod::Sim => "SIM",
423 EapMethod::Aka => "AKA",
424 EapMethod::AkaPrime => "AKA_PRIME",
425 };
426 fields.push(Field::new_string("E", eap_name));
427 }
428}
429
430#[derive(Debug, Clone)]
431#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
432#[non_exhaustive]
433pub enum Phase2 {
434 MsChap,
435 MsChapV2,
436 Pap,
437 Gtc,
438 Sim,
439 Aka,
440 AkaPrime,
441}
442
443impl Phase2 {
444 pub fn add_fields(&self, fields: &mut Vec<Field>) {
445 let ph2_name = match self {
446 Phase2::MsChap => "MSCHAP",
447 Phase2::MsChapV2 => "MSCHAPV2",
448 Phase2::Gtc => "GTC",
449 Phase2::Sim => "SIM",
450 Phase2::Aka => "AKA",
451 Phase2::AkaPrime => "AKA_PRIME",
452 Phase2::Pap => "PAP",
453 };
454 fields.push(Field::new_string("PH2", ph2_name));
455 }
456}