use issuerd_core::IssuerdError;
pub(crate) fn qr_svg(data: &str) -> Result<String, IssuerdError> {
const BORDER: i32 = 4;
let qr = qrcodegen::QrCode::encode_text(data, qrcodegen::QrCodeEcc::Medium)
.map_err(|e| IssuerdError::ServerError(format!("QR encoding failed: {e}")))?;
let dimension = qr.size() + BORDER * 2;
let mut path = String::with_capacity(qr.size() as usize * qr.size() as usize * 14);
for y in 0..qr.size() {
for x in 0..qr.size() {
if qr.get_module(x, y) {
if !path.is_empty() {
path.push(' ');
}
path.push_str(&format!("M{},{}h1v1h-1z", x + BORDER, y + BORDER));
}
}
}
Ok(format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" \
viewBox=\"0 0 {dimension} {dimension}\" stroke=\"none\">\
<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\
<path d=\"{path}\" fill=\"#000000\"/></svg>"
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qr_svg_produces_svg_markup() {
let input = "otpauth://totp/issuer:alice?secret=ABC&issuer=issuer";
let svg = qr_svg(input).unwrap();
assert!(svg.starts_with("<svg"), "expected svg element, got: {svg}");
assert!(svg.contains("<path d=\"M"), "expected qr modules path");
let qr = qrcodegen::QrCode::encode_text(input, qrcodegen::QrCodeEcc::Medium).unwrap();
let dimension = qr.size() + 8;
assert!(
svg.contains(&format!("viewBox=\"0 0 {dimension} {dimension}\"")),
"expected {dimension}x{dimension} viewBox: {svg}"
);
}
#[test]
fn qr_svg_rejects_oversized_input() {
let huge = "x".repeat(10_000);
assert!(qr_svg(&huge).is_err());
}
}